Subject: Re: Statistics Import and Export

From: Corey Huinker <[email-masked]>
Date: Mon, 24 Feb 2025 05:11:48 -0500

>
> 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.
From 95127f6fd82bde843e5840de93678ff784750c8a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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

From 5a03d2935999cac037daeaac566e7709f1824f5e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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

From 02bb2a98e7e290c53e3e130b330073d56af93f53 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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

---

From: Andres Freund <[email-masked]>
Date: Mon, 24 Feb 2025 09:54:22 -0500

Hi,

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

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

>  	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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Feb 2025 08:15:25 -0800

> 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

---

From: Tom Lane <[email-masked]>
Date: Mon, 24 Feb 2025 12:50:58 -0500

> 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].
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] https://www.postgresql.org/message-id/816167.1740278884%40sss.pgh.pa.us

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,

---

From: Corey Huinker <[email-masked]>
Date: Mon, 24 Feb 2025 12:57:17 -0500

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

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

> That query is already querying relpages a bit later in the query, so we'd

+1, must eliminate that duplicate.

> any uses of it yet in pg_dump.

+1, I would like to see that change.

> 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

I have not.

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Feb 2025 10:42:40 -0800

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

> 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

---

From: Corey Huinker <[email-masked]>
Date: Mon, 24 Feb 2025 13:47:19 -0500

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

> handling of the successive output values as was discussed upthread.

Can do.

> and have one function that executes a query and immediately interprets

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

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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Feb 2025 10:54:43 -0800

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

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

---

From: Tom Lane <[email-masked]>
Date: Mon, 24 Feb 2025 14:34:13 -0500

> 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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Feb 2025 11:45:51 -0800

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

---

From: Tom Lane <[email-masked]>
Date: Mon, 24 Feb 2025 15:03:58 -0500

> much to weigh that concern, but I agree that there is non-zero

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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Feb 2025 12:20:41 -0800

On Mon, 2025-02-24 at 15:03 -0500, Tom Lane wrote:
> 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

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"));

---

From: Andres Freund <[email-masked]>
Date: Mon, 24 Feb 2025 15:36:20 -0500

Hi,

> 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

---

From: Tom Lane <[email-masked]>
Date: Mon, 24 Feb 2025 15:40:13 -0500

Jeff Davis <[email-masked]> writes:
> 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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Feb 2025 12:41:21 -0800

On Mon, 2025-02-24 at 15:36 -0500, Andres Freund wrote:
> 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

---

From: Corey Huinker <[email-masked]>
Date: Mon, 24 Feb 2025 15:45:10 -0500

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

---

From: Andres Freund <[email-masked]>
Date: Mon, 24 Feb 2025 15:53:04 -0500

Hi,

> 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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Feb 2025 13:01:00 -0800

> 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

---

From: Corey Huinker <[email-masked]>
Date: Mon, 24 Feb 2025 16:01:09 -0500

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

> to avoid it. We should work to get rid of them, not introduce them in more

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?

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Feb 2025 13:07:39 -0800

> 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

---

From: Corey Huinker <[email-masked]>
Date: Mon, 24 Feb 2025 16:13:41 -0500

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

---

From: jian he <[email-masked]>
Date: Tue, 25 Feb 2025 11:30:12 +0800

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

---

From: Ashutosh Bapat <[email-masked]>
Date: Tue, 25 Feb 2025 11:11:55 +0530

Hi Jeff and Corey,

I think I have found a bug (arguably) with the dump/restore test I am
developing at [1].

#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] https://www.postgresql.org/message-id/CAExHW5sBbMki6Xs4XxFQQF3C4Wx3wxkLAcySrtuW3vrnOxXDNQ%40mail.gmail.com

-- 
Best Wishes,
Ashutosh Bapat

---

From: Jeff Davis <[email-masked]>
Date: Tue, 25 Feb 2025 00:10:23 -0800

> elog(ERROR, "pg_class entry for relid %u not found", reloid)));

Thank you.

> 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

---

From: Jeff Davis <[email-masked]>
Date: Tue, 25 Feb 2025 10:22:13 -0800

> 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

From ea413ee48b10299530bafc3102395285b5ea8ce3 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email-masked]>
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/[email-masked]
---
 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

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Feb 2025 15:31:40 -0500

> rework, but I think it's for the better now that we aren't trying to do
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:

+         type <type>real</type>, and <literal>relallvisible</literal> with
a

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.

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Feb 2025 18:40:34 -0500

> * 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.
From 30e9010a54d43376af2b33281974c1ebb0ea6382 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email-masked]>
Date: Mon, 24 Feb 2025 17:24:05 -0800
Subject: [PATCH vAdios-Set 1/3] Remove redundant pg_set_*_stats() variants.

 doc/src/sgml/func.sgml                     | 260 +++----
 9 files changed, 212 insertions(+), 1734 deletions(-)

 

 
 
 	/*

 

 
 
 		if (converted)
 		{
 
 		if (converted)
 
 		if (converted)
 		{
 
 		if (converted)
 		{
 
 		if (converted)
 		{

 
-/*

 
 	PG_RETURN_BOOL(result);

 /*
 
-/*

 

 	PG_RETURN_BOOL(result);

 	if (ARR_NDIM(arr) != 1)
 	{
-		ereport(elevel,
+		ereport(WARNING,

-		ereport(elevel,
+		ereport(WARNING,

-	ereport(elevel,
+	ereport(WARNING,
 
 	return -1;

 

--- positional arguments
-SELECT
 
 SELECT

 

 

--- positional arguments
-SELECT
 
 SELECT

 SELECT

 
 
--- error: attname null

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

From 925176e7524291dea63d69e0893d236491894663 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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

From b23e5bae0c0e650ec76cffa7343cb17b0295e4d5 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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

---

From: Jeff Davis <[email-masked]>
Date: Tue, 25 Feb 2025 16:17:37 -0800

On Tue, 2025-02-25 at 15:31 -0500, Corey Huinker wrote:
> 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.

> Do we want to re-compact the oids we consumed in pg_proc.dat?

Done.

> * cannot set most_common_elems for range type

Done.

And committed.

Regards,
	Jeff Davis

---

From: Jeff Davis <[email-masked]>
Date: Tue, 25 Feb 2025 18:00:12 -0800

> Have you compared performance of with/without stats after these

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

From d617fb142158e0ca964e5bc8bb3351d993de6062 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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 <[email-masked]>
Co-authored-by: Jeff Davis <[email-masked]>
Discussion: https://postgr.es/m/CADkLM=[email-masked]
---
 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);
 
 		/*
+			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;

 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -10383,18 +10410,6 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
-/*
@@ -10419,30 +10434,6 @@ static const char *att_stats_arginfo[][2] = {
 
-/*
- * 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();
 	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 */
+	int32		relpages;
+	float		reltuples;
+	int32		relallvisible;

From 33cde5fdbb207a99bce678015e1e45df0210bb56 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email-masked]>
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 <[email-masked]>
Co-authored-by: Corey Huinker <[email-masked]>
Co-authored-by: Jeff Davis <[email-masked]>
Discussion: https://postgr.es/m/CADkLM=[email-masked]
---
 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;
 
@@ -10622,15 +10572,56 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
-	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);

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Feb 2025 21:29:56 -0500

> I plan to commit the patches soon.
+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.

---

From: Tom Lane <[email-masked]>
Date: Tue, 25 Feb 2025 22:40:06 -0500

> 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

---

From: Jeff Davis <[email-masked]>
Date: Tue, 25 Feb 2025 19:55:19 -0800

> that once Jeff pushes these patches, unless one of you are already

I just committed them.

Regards,
	Jeff Davis

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Feb 2025 23:05:57 -0500

> that once Jeff pushes these patches, unless one of you are already

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.

---

From: Tom Lane <[email-masked]>
Date: Tue, 25 Feb 2025 23:36:09 -0500

> stats (at least for now) and wouldn't be affected by the name-jitter issue

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.

> 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

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Feb 2025 23:57:40 -0500

> bother to rename index columns when the underlying table column is

Ouch.

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

> use for that.  I won't stand in the way if you're desperate to

I'm not desperate to slow this thread down, no. We'll stick with
attname-only.

---

From: Tom Lane <[email-masked]>
Date: Wed, 26 Feb 2025 00:05:44 -0500

> the aggregated array, and there's no need to do a parallel array_agg of

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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 26 Feb 2025 04:25:29 -0500

> to store anything in the common case.

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
From 9dd7129318804ede9e412d886f3124e6d452a2d9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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(-)

index 66a5676c810..71a7a175d1f 100644
+	[ATTNUM_ARG] = {"attnum", INT2OID},
 
+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);
 
+		attnum = PG_GETARG_INT16(ATTNUM_ARG);
 
 	if (attnum < 0)
 		ereport(ERROR,
 
@@ -241,7 +289,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 			ereport(WARNING,
@@ -257,7 +305,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 		ereport(WARNING,
 
 		do_histogram = false;
@@ -271,7 +319,7 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	{
 		ereport(WARNING,
 
 		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_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);
 
index 7e8b7f429c9..b8fc2fcce46 100644
@@ -1250,7 +1250,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(

base-commit: 0e42d31b0b2273c376ce9de946b59d155fac589c
-- 
2.48.1

From 92f75fc9835e7982216d32329caa34b26347a6b3 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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)

-	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

---

From: Tom Lane <[email-masked]>
Date: Wed, 26 Feb 2025 11:13:54 -0500

> * Folds appendRelStatsImport and appendAttStatsImport

Cool.  Jeff, are you taking these, or shall I?

			regards, tom lane

---

From: Jeff Davis <[email-masked]>
Date: Wed, 26 Feb 2025 08:16:31 -0800

> 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

---

From: Tom Lane <[email-masked]>
Date: Wed, 26 Feb 2025 11:23:35 -0500

> 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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 26 Feb 2025 11:43:03 -0500

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

> such, so I put the idea on the back burner.

I didn't even think to try const-ing those structs.

---

From: Jeff Davis <[email-masked]>
Date: Wed, 26 Feb 2025 10:02:13 -0800

> * Basically what Tom posted earlier, minus the pg_set_attribute_stats

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.

> * Folds appendRelStatsImport and appendAttStatsImport

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:

https://www.postgresql.org/message-id/[email-masked]

Regards,
	Jeff Davis

---

From: Tom Lane <[email-masked]>
Date: Wed, 26 Feb 2025 13:06:26 -0500

> https://www.postgresql.org/message-id/[email-masked]

Thanks for doing that.  I agree with your other comments and
will incorporate them.

			regards, tom lane

---

From: Jeff Davis <[email-masked]>
Date: Wed, 26 Feb 2025 10:44:33 -0800

> Thanks for doing that.  I agree with your other comments and

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

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 26 Feb 2025 14:54:16 -0500

On Tue, Feb 25, 2025 at 6:41 PM Corey Huinker <[email-masked]> 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

---

From: Robert Haas <[email-masked]>
Date: Wed, 26 Feb 2025 15:15:19 -0500

> 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

---

From: Jeff Davis <[email-masked]>
Date: Wed, 26 Feb 2025 12:37:47 -0800

On Wed, 2025-02-26 at 15:15 -0500, Robert Haas wrote:
> 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

---

From: Robert Haas <[email-masked]>
Date: Wed, 26 Feb 2025 15:57:44 -0500

> Removed already in commit f3dae2ae58.

Cool.

> With that in mind, should we remove the in-place updates from ANALYZE

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.

---

From: Jeff Davis <[email-masked]>
Date: Wed, 26 Feb 2025 13:32:26 -0800

On Wed, 2025-02-26 at 15:57 -0500, Robert Haas wrote:
> If you feel it can be done without breaking anything else,

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

---

From: Tom Lane <[email-masked]>
Date: Wed, 26 Feb 2025 16:44:20 -0500

> 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

---

From: Tom Lane <[email-masked]>
Date: Wed, 26 Feb 2025 16:46:11 -0500

> 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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 26 Feb 2025 16:57:34 -0500

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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 26 Feb 2025 16:58:04 -0500

> clear now if you want to go do that.

On it.

---

From: Corey Huinker <[email-masked]>
Date: Wed, 26 Feb 2025 21:19:12 -0500

>> clear now if you want to go do that.

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
From f3087b04784d6853970bf41eb619281d72ce94bd Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 27 Feb 2025 18:27:55 -0500

>>> clear now if you want to go do that.

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:

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

---

From: Jeff Davis <[email-masked]>
Date: Thu, 27 Feb 2025 18:32:20 -0800

> dumping statistics only to get rewritten even before restore

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

*:
https://www.postgresql.org/message-id/1798867.1712376328%40sss.pgh.pa.us
From 86c03cb525b49d24019c5c0ea8ec36bb82b3c58a Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email-masked]>
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 <[email-masked]>
Discussion: https://postgr.es/m/CAExHW5vf9D+8-a5_BEX3y=2y_xY9hiCxV1=[email-masked]
---
 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);

---

From: Corey Huinker <[email-masked]>
Date: Thu, 27 Feb 2025 22:01:00 -0500

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

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

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

---

From: Greg Sabino Mullane <[email-masked]>
Date: Thu, 27 Feb 2025 22:42:17 -0500

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

---

From: Corey Huinker <[email-masked]>
Date: Thu, 27 Feb 2025 23:34:40 -0500

> 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.
From d0dfca62eb8ce27fb4bfce77bd2ee835738899a8 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 26 Feb 2025 21:02:44 -0500
Subject: [PATCH v2] Organize and deduplicate statistics import tests.

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

index 1f150f7b08d..7bd7bfb3e7b 100644
@@ -13,19 +13,48 @@ CREATE TABLE stats_import.test(
+        'relpages', 17::integer);
+        'relpages', 17::integer);
+        NULL, '17'::integer);
+ERROR:  name at variadic position 3 is NULL
+        17, '17'::integer);
+ERROR:  name at variadic position 3 has type "integer", expected type "text"
 
-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
@@ -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
 
+-- ok: set all relstats, with version, no bounds checking
@@ -181,13 +152,12 @@ FROM pg_class
 
--- 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)
 
+-- ok: just the fixed values, with version, no stakinds
@@ -297,15 +372,16 @@ AND attname = 'id';
 
@@ -322,14 +398,12 @@ AND attname = 'id';
 
@@ -348,15 +422,12 @@ AND attname = 'id';
 
@@ -373,18 +444,15 @@ AND inherited = false
 
@@ -401,18 +469,15 @@ AND inherited = false
 
@@ -431,18 +496,15 @@ AND inherited = false
 
@@ -460,7 +522,7 @@ AND inherited = false
 
 -- ok: mcv+mcf
@@ -468,10 +530,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -488,18 +546,15 @@ AND inherited = false
 
@@ -516,7 +571,7 @@ AND inherited = false
 
 -- ok: histogram_bounds
@@ -524,11 +579,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -542,19 +594,16 @@ AND inherited = false
 
@@ -570,7 +619,7 @@ AND inherited = false
 
@@ -578,10 +627,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -597,18 +643,15 @@ AND inherited = false
 
@@ -627,18 +670,15 @@ AND inherited = false
 
@@ -655,18 +695,15 @@ AND inherited = false
 
@@ -683,7 +720,7 @@ AND inherited = false
 
@@ -691,10 +728,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -711,18 +744,15 @@ AND inherited = false
 
@@ -740,7 +770,7 @@ AND inherited = false
 
@@ -748,10 +778,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -767,26 +793,17 @@ AND inherited = false
 
@@ -801,19 +818,17 @@ WHERE schemaname = 'stats_import'
 
@@ -832,17 +847,15 @@ AND inherited = false
 
@@ -859,17 +872,15 @@ AND inherited = false
 
@@ -878,14 +889,22 @@ WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is
  f
 (1 row)
 
+SELECT *
+FROM pg_stats
@@ -902,18 +921,16 @@ AND inherited = false
 
@@ -930,43 +947,7 @@ AND inherited = false
 
 --
@@ -986,19 +967,6 @@ SELECT 3, 'tre', (3, 3.3, 'TRE', '2003-03-03', NULL)::stats_import.complex_type,
 UNION ALL
@@ -1176,7 +1144,18 @@ WHERE s.starelid = 'stats_import.is_odd'::regclass;
 
--- ok
@@ -1186,154 +1165,17 @@ SELECT pg_catalog.pg_clear_attribute_stats(
  
 (1 row)
 
---
--- Negative tests
---
index 8c183bceb8a..7b2c7d6617f 100644
@@ -17,15 +17,43 @@ CREATE TABLE stats_import.test(

+--
+-- relstats tests
+--
+
+        'relpages', 17::integer);
+        'relpages', 17::integer);
+        NULL, '17'::integer);
+        17, '17'::integer);
 
-BEGIN;
         'relpages', 18::integer);
 
@@ -39,20 +67,6 @@ WHERE relation = 'stats_import.test_i'::regclass AND
 
 COMMIT;
 
-SELECT
@@ -68,18 +82,6 @@ SELECT relpages
 
@@ -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
 
+-- ok: set all relstats, with version, no bounds checking
 
@@ -126,40 +120,36 @@ SELECT relpages, reltuples, relallvisible
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just relpages
         'relpages', '16'::integer);

--- ok: just reltuples
         'reltuples', '500'::real);

         'relallvisible', 5::integer);

         'reltuples', 400.0::real,
@@ -168,17 +158,110 @@ SELECT relpages, reltuples, relallvisible

--- ok: no stakinds
+--
+-- attribute stats
+--
+
+-- ok: just the fixed values, with version, no stakinds
@@ -195,15 +278,16 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

 SELECT *
 FROM pg_stats
@@ -212,14 +296,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

 SELECT *
@@ -229,15 +311,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -248,15 +327,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -267,15 +343,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
@@ -287,15 +360,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
@@ -312,10 +382,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -327,15 +393,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -351,11 +414,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
 SELECT *
 FROM pg_stats
@@ -364,16 +424,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

 SELECT *
@@ -388,10 +445,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
@@ -402,15 +456,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
@@ -422,15 +473,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -441,15 +489,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';

@@ -465,10 +510,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -480,15 +521,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';

@@ -504,10 +542,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
@@ -518,23 +552,14 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';

 SELECT *
@@ -544,14 +569,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
@@ -563,14 +586,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -581,25 +602,27 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';

+SELECT *
+FROM pg_stats
@@ -611,15 +634,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';

 SELECT *
@@ -629,32 +650,6 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
@@ -674,15 +669,6 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;

 
@@ -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
 
---
--- Negative tests
---
-

base-commit: c2a50ac678eb5ccee271aef3e7ed146ac395a32b
-- 
2.48.1

---

From: Ashutosh Bapat <[email-masked]>
Date: Fri, 28 Feb 2025 14:51:29 +0530

Hi Jeff,
I am changing the subject on this email and thus creating a new thread
to discuss this issue.

> test in 71b66171d0 to only update the stats if the physical relpages is

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.

> -no-statistics, and then with --statistics-only, and restore the two

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

> already discussed[*], and we decided against it (though there was not a

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

-- 
Best Wishes,
Ashutosh Bapat

---

From: Jeff Davis <[email-masked]>
Date: Fri, 28 Feb 2025 12:10:37 -0800

> original and restored database - which may have other effects like

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

From a8945b9ce4e358f4a79d3065c07f3b42a94fd387 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email-masked]>
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 <[email-masked]>
Discussion: https://postgr.es/m/CAExHW5vf9D+8-a5_BEX3y=2y_xY9hiCxV1=[email-masked]
---
 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
+-- CREATE INDEX on a table with autovac disabled should not overwrite
+-- stats
 
+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;

---

From: Jeff Davis <[email-masked]>
Date: Fri, 28 Feb 2025 12:54:03 -0800

On Thu, 2025-02-27 at 22:42 -0500, Greg Sabino Mullane wrote:
> 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] 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]
https://www.postgresql.org/message-id/3228677.1713844341%40sss.pgh.pa.us

---

From: Alexander Lakhin <[email-masked]>
Date: Sat, 1 Mar 2025 19:00:00 +0200

Hello Jeff,

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

---

From: Tom Lane <[email-masked]>
Date: Sat, 01 Mar 2025 13:04:21 -0500

> https://buildfarm.postgresql.org/cgi-bin/show_log.pl?nm=fairywren&dt=2025-02-26%2010%3A03%3A07

Yeah, Andrew and I have been puzzling over that off-list.  pg_dump
is definitely exiting unceremoniously.

>                 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

---

From: Alexander Lakhin <[email-masked]>
Date: Sat, 1 Mar 2025 20:20:00 +0200

Hello Tom,

> what do you think is wrong exactly?  I see nothing obviously

 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)

---

From: Greg Sabino Mullane <[email-masked]>
Date: Sat, 1 Mar 2025 13:52:07 -0500

> 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

---

From: Tom Lane <[email-masked]>
Date: Sat, 01 Mar 2025 13:56:51 -0500

>  From my understanding, pg_strtof () can't stand against endptr == NULL.

D'oh!  I'm blind as a bat today.

> 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

---

From: Jeff Davis <[email-masked]>
Date: Sat, 01 Mar 2025 12:48:18 -0800

On Sat, 2025-03-01 at 13:52 -0500, Greg Sabino Mullane wrote:
> 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

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

---

From: Tom Lane <[email-masked]>
Date: Sat, 01 Mar 2025 16:23:39 -0500

> That's an interesting point. What tools are currrently trying to parse

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

---

From: Corey Huinker <[email-masked]>
Date: Sat, 1 Mar 2025 20:55:51 -0500

> uncomfortable about the amount of baggage we added to pg_dump

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.

---

From: Magnus Hagander <[email-masked]>
Date: Sun, 2 Mar 2025 15:38:07 +0100

> 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 suggest that we wait a bit to see what additional feedback we get

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.

> > setval" and "SELECT set_config" are single line, but there may be

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/>
 Work: https://www.redpill-linpro.com/ <http://www.redpill-linpro.com/>

---

From: Corey Huinker <[email-masked]>
Date: Sun, 2 Mar 2025 15:29:17 -0500

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

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

---

From: Ashutosh Bapat <[email-masked]>
Date: Mon, 3 Mar 2025 22:04:16 +0530

> 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

---

From: Greg Sabino Mullane <[email-masked]>
Date: Mon, 3 Mar 2025 14:00:35 -0500

On Sat, Mar 1, 2025 at 4:23 PM Tom Lane <[email-masked]> 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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 03 Mar 2025 16:55:46 -0800

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

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

---

From: Ashutosh Bapat <[email-masked]>
Date: Tue, 4 Mar 2025 10:28:16 +0530

> 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

> 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

---

From: Jeff Davis <[email-masked]>
Date: Tue, 04 Mar 2025 10:15:30 -0800

On Tue, 2025-03-04 at 10:28 +0530, Ashutosh Bapat wrote:

> 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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 5 Mar 2025 03:08:55 -0500

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] 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]. 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], and reduce customer confusion concerning whether
they are in need of post-upgrade analyzing of anything.

[1]
https://www.postgresql.org/message-id/flat/CACJufxFVq%3Dtq9u1zrHWYSbMi1T07gS9Ff0LJScMco4HZmtZ1xw%40mail.gmail.com
[2]
https://www.postgresql.org/message-id/flat/CAExHW5sNgxkqkyscm9KRrcwvi%2B_Hg%3DPRe_u%2BxZYJzX%2Bw4XAMjQ%40mail.gmail.com#93c77f59150be1471c38f1a315772215
[3]
https://www.postgresql.org/message-id/flat/CADkLM%3DdnFKZMAo7MwqD2X6JjiiLCoVXHmszqtZp8sycYmoCcMQ%40mail.gmail.com#eff41d39f7158086e8398803165c5c2f
[4]
https://www.postgresql.org/message-id/flat/CANWCAZZf_jNn2i%2B6-JfQt_j909DBk-U6Dg0M7iArZPLgdXCAmw%40mail.gmail.com#e575b5bede51455f406ac3872d2f82ec
From 0ed0773d920f96e2f1d2e054a81f364d45d4826d Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
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

From 4fb95c2dc726d666fe32a59595d5dc2478b80465 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 26 Feb 2025 21:02:44 -0500
Subject: [PATCH v5 2/3] Organize and deduplicate statistics import tests.

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

index 4df287e547f..ebba14c6a1d 100644
 SELECT relpages, reltuples, relallvisible, relallfrozen
  relpages | reltuples | relallvisible | relallfrozen 
 ----------+-----------+---------------+--------------
-        0 |        -1 |             0 |            0
+        1 |         0 |             0 |            0
 (1 row)
 
-BEGIN;
 
 COMMIT;
-SELECT
-SELECT relpages, reltuples, relallvisible, relallfrozen
- relpages | reltuples | relallvisible | relallfrozen 
-----------+-----------+---------------+--------------
-        0 |        -1 |             0 |            0

 COMMIT;
-SELECT
 
--- 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
@@ -203,10 +173,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       400 |             4 |            2
 (1 row)
 
--- ok: just reltuples
@@ -221,10 +190,9 @@ WHERE oid = 'stats_import.test'::regclass;
        16 |       500 |             4 |            2
 (1 row)
 
@@ -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)
 
@@ -317,15 +386,16 @@ AND attname = 'id';
 
@@ -342,14 +412,12 @@ AND attname = 'id';
 
@@ -368,15 +436,12 @@ AND attname = 'id';
 
@@ -393,18 +458,15 @@ AND inherited = false
 
@@ -421,18 +483,15 @@ AND inherited = false
 
@@ -451,18 +510,15 @@ AND inherited = false
 
@@ -480,7 +536,7 @@ AND inherited = false
 
 -- ok: mcv+mcf
@@ -488,10 +544,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -508,18 +560,15 @@ AND inherited = false
 
@@ -536,7 +585,7 @@ AND inherited = false
 
 -- ok: histogram_bounds
@@ -544,11 +593,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -562,19 +608,16 @@ AND inherited = false
 
@@ -590,7 +633,7 @@ AND inherited = false
 
@@ -598,10 +641,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -617,18 +657,15 @@ AND inherited = false
 
@@ -647,18 +684,15 @@ AND inherited = false
 
@@ -675,18 +709,15 @@ AND inherited = false
 
@@ -703,7 +734,7 @@ AND inherited = false
 
@@ -711,10 +742,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -731,18 +758,15 @@ AND inherited = false
 
@@ -760,7 +784,7 @@ AND inherited = false
 
@@ -768,10 +792,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -787,26 +807,17 @@ AND inherited = false
 
@@ -821,19 +832,17 @@ WHERE schemaname = 'stats_import'
 
@@ -852,17 +861,15 @@ AND inherited = false
 
@@ -879,17 +886,15 @@ AND inherited = false
 
@@ -898,14 +903,22 @@ WARNING:  "most_common_elems" must be specified when "most_common_elem_freqs" is
  f
 (1 row)
 
+SELECT *
+FROM pg_stats
@@ -922,18 +935,16 @@ AND inherited = false
 
@@ -950,43 +961,7 @@ AND inherited = false
 
 --
@@ -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
@@ -1207,160 +1179,17 @@ SELECT pg_catalog.pg_clear_attribute_stats(
  
 (1 row)
 
---
--- Negative tests
---
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-        'relallfrozen', 3::integer,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
index febda3d18d9..8d04ff4f378 100644

+--
+-- relstats tests
+--
+
 SELECT relpages, reltuples, relallvisible, relallfrozen
 
-BEGIN;

 COMMIT;
 
-SELECT
-SELECT relpages, reltuples, relallvisible, relallfrozen

-SELECT

 COMMIT;
 
-SELECT
 
--- 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
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
--- ok: just reltuples
 
 SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;

 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -167,10 +158,9 @@ SELECT relpages, reltuples, relallvisible, relallfrozen
 FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
         'relallvisible', 4::integer,
@@ -180,17 +170,104 @@ SELECT relpages, reltuples, relallvisible, relallfrozen

--- ok: no stakinds
+--
+-- attribute stats
+--
+
@@ -207,15 +284,16 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

 SELECT *
 FROM pg_stats
@@ -224,14 +302,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

 SELECT *
@@ -241,15 +317,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -260,15 +333,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -279,15 +349,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
@@ -299,15 +366,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
@@ -324,10 +388,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -339,15 +399,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -363,11 +420,8 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
 SELECT *
 FROM pg_stats
@@ -376,16 +430,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

 SELECT *
@@ -400,10 +451,7 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
@@ -414,15 +462,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';
 
@@ -434,15 +479,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -453,15 +495,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';

@@ -477,10 +516,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
@@ -492,15 +527,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';

@@ -516,10 +548,6 @@ SELECT pg_catalog.pg_restore_attribute_stats(
 
@@ -530,23 +558,14 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';

 SELECT *
@@ -556,14 +575,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'arange';
 
@@ -575,14 +592,12 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';

@@ -593,25 +608,27 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';

+SELECT *
+FROM pg_stats
@@ -623,15 +640,13 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'tags';

 SELECT *
@@ -641,32 +656,6 @@ AND tablename = 'test'
 AND inherited = false
 AND attname = 'id';
 
@@ -686,16 +675,6 @@ SELECT 4, 'four', NULL, int4range(0,100), NULL;

-    'relallvisible', '0'::integer,
-    'relallfrozen', '0'::integer
 
@@ -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
 
---
--- Negative tests
---
-
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);
-        'relallfrozen', 3::integer,
-        'relallvisible', 4::integer,
-        'relallfrozen', 3::integer);

From 87ab5a7a5061ba1b064bf81f115bb5c4204d0fc6 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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_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);
 
index 2c1cea3fc80..4c1e75a3c78 100644
@@ -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)

-	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)
 
+    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: schemaname missing
+    'relname', 'test',
+    'attname', 'id',
     '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);
 
+    'schemaname', 'stats_import',
+    'relname', 'test',
+    'attname', 'xmin',
     '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';
 
+    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

---

From: Ashutosh Bapat <[email-masked]>
Date: Wed, 5 Mar 2025 15:22:56 +0530

> SQL, it reads nicely to me and there's no obvious problem with it.

Thanks for explaining it. I

> 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

---

From: Andres Freund <[email-masked]>
Date: Wed, 5 Mar 2025 20:17:53 -0500

Hi,

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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 5 Mar 2025 20:30:48 -0500

> non-expression indexes. In most cases that'll already drastically cut down

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.

---

From: Nathan Bossart <[email-masked]>
Date: Wed, 5 Mar 2025 19:36:29 -0600

> non-expression indexes. In most cases that'll already drastically cut down on

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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 5 Mar 2025 20:54:35 -0500

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

---

From: Andres Freund <[email-masked]>
Date: Wed, 5 Mar 2025 21:18:17 -0500

Hi,

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

> 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

---

From: Nathan Bossart <[email-masked]>
Date: Wed, 5 Mar 2025 20:19:36 -0600

> 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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 5 Mar 2025 22:00:42 -0500

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

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

> required stats should probably use PQsetSingleRowMode() or

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.

---

From: Andres Freund <[email-masked]>
Date: Wed, 5 Mar 2025 22:41:24 -0500

Hi,

> suggesting we back the joins from pg_stats to pg_namespace and pg_class and

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.

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

> 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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 5 Mar 2025 23:04:29 -0500

> Not sure how good the query plan for that would be, but it may be worth

Ok, so we're willing to take the pg_class/pg_namespace join hit for one or
a handful of queries, good to know.

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

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

> 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.
From 87ec2ad2f7dba7cce39a959ef02413be68caf4fb Mon Sep 17 00:00:00 2001
From: jian he <[email-masked]>
Date: Tue, 25 Feb 2025 14:13:24 +0800
Subject: [PATCH v6 1/4] refactor _tocEntryRequired for STATISTICS DATA.

 

base-commit: 39de4f157d3ac0b889bb276c2487fe160578f967
-- 
2.48.1

From 2b4bce20e5e9f1b1fc68057e649dacb91f6bb6c9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Wed, 26 Feb 2025 21:02:44 -0500
Subject: [PATCH v6 2/4] Organize and deduplicate statistics import tests.

 
-BEGIN;
 
 COMMIT;
-SELECT

 COMMIT;
-SELECT

--- ok: just relpages
 
--- ok: just reltuples

 

 

 
 
 -- ok: mcv+mcf

 -- ok: histogram_bounds

 

 

 

 
 
+SELECT *
+FROM pg_stats

 
--- ok
  
 (1 row)
 
---
--- Negative tests
---

+--
+-- relstats tests
+--
+
 
-BEGIN;

 COMMIT;
 
-SELECT

-SELECT

 COMMIT;
 
-SELECT

--- ok: just relpages

--- ok: just reltuples

 

 
--- ok: no stakinds
+--
+-- attribute stats
+--
+

 SELECT *
 FROM pg_stats

 SELECT *

 

 

 
 SELECT *
 FROM pg_stats

 SELECT *

 

 

 

 SELECT *

 

+SELECT *
+FROM pg_stats

 SELECT *

 

--- ok
 
---
--- Negative tests
---
-

From 5ea8a35a1d6db14046bb2fb76dfcdddb1bcf9479 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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;
 
 		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

From cb22c5782c4652d76885cb8dd3a2753ccac01b24 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v6 3/4] Split relation into schemaname and relname.

index 134b3dd8689..53aa4bc4df3 100644

 

 
 
 	bool		result = true;

 

 
 
 	if (attnum < 0)
 		ereport(ERROR,

 

@@ -32,7 +35,8 @@

 

 

 

-	free(qualified_name);
 	destroyPQExpBuffer(out);

 

 -- ok: just relallfrozen

 
 -- ok: clear stats

 

 
 
 -- ok: mcv+mcf

 -- ok: histogram_bounds

 

 

 

 
 
 -- ok: mcelem

  

 

 

 

 

 
 
 -- ok: just relallfrozen

 

 -- ok: clear stats

 

 
 --
 -- attribute stats
 --

--- error: relation null
 
 -- error: NULL attname

 

 -- error: inherited null

 

 
 
 -- ok: mcv+mcf

 -- ok: histogram_bounds

 

 

 

 
 
 -- ok: mcelem

 
 SELECT COUNT(*)
 FROM pg_stats
index f97f0ce570a..4c5ef0604de 100644
@@ -30331,22 +30331,24 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
@@ -30374,7 +30376,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
@@ -30423,16 +30425,18 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
@@ -30468,7 +30472,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset

---

From: Jeff Davis <[email-masked]>
Date: Wed, 05 Mar 2025 22:52:08 -0800

> 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

---

From: Jeff Davis <[email-masked]>
Date: Thu, 06 Mar 2025 00:07:01 -0800

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

---

From: Jeff Davis <[email-masked]>
Date: Thu, 06 Mar 2025 00:48:40 -0800

> patches, plus the optimization of avoiding querying on non-expression

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

---

From: Corey Huinker <[email-masked]>
Date: Thu, 6 Mar 2025 08:49:33 -0500

> inconsistent with its respective catalog. I assume that's the

Correct, either we use 'tablename' to describe indexes as well, or we
diverge from the system view's naming.

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

> makes sense, so I'm inclined to move ahead with it, but I'm open to

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

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

---

From: Andres Freund <[email-masked]>
Date: Thu, 6 Mar 2025 09:29:07 -0500

Hi,

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

> 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

---

From: Robert Haas <[email-masked]>
Date: Thu, 6 Mar 2025 11:15:32 -0500

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

---

From: Corey Huinker <[email-masked]>
Date: Thu, 6 Mar 2025 12:04:25 -0500

> > Pardon my inexperience, but aren't the ArchiveEntry records needed right

yes

> stats in a single query would also require keeping them in memory is not an

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.

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

---

From: Andres Freund <[email-masked]>
Date: Thu, 6 Mar 2025 12:16:28 -0500

Hi,

On 2025-03-06 12:04:25 -0500, Corey Huinker wrote:
> 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?

> Yes, it will cost us in pg_dump, but it will save customers from some long

My concern is that it might prevent some upgrades from *ever* completing,
because of pg_dump running out of memory.

Greetings,

Andres Freund

---

From: Corey Huinker <[email-masked]>
Date: Thu, 6 Mar 2025 12:16:44 -0500

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

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.

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

---

From: Jeff Davis <[email-masked]>
Date: Thu, 06 Mar 2025 09:59:34 -0800

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

---

From: Corey Huinker <[email-masked]>
Date: Thu, 6 Mar 2025 13:00:07 -0500

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

> because of pg_dump running out of memory.

Obviously a valid concern.

---

From: Andres Freund <[email-masked]>
Date: Thu, 6 Mar 2025 13:04:55 -0500

Hi,

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

> potentially saving the customer hours of reduced availability if not

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

---

From: Jeff Davis <[email-masked]>
Date: Thu, 06 Mar 2025 10:07:43 -0800

> 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

---

From: Andres Freund <[email-masked]>
Date: Thu, 6 Mar 2025 13:08:20 -0500

Hi,

On 2025-03-06 13:00:07 -0500, Corey Huinker wrote:
> 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

---

From: Nathan Bossart <[email-masked]>
Date: Thu, 6 Mar 2025 12:22:12 -0600

On Thu, Mar 06, 2025 at 01:04:55PM -0500, Andres Freund wrote:
> 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.

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

> 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

---

From: Corey Huinker <[email-masked]>
Date: Thu, 6 Mar 2025 13:23:36 -0500

> expect pg_dump to create temp tables, but I can't think of a major

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

getPolicies - as Andres cited right after your post.

---

From: Andres Freund <[email-masked]>
Date: Thu, 6 Mar 2025 13:23:40 -0500

Hi,

> expect pg_dump to create temp tables, but I can't think of a major

It doesn't work on a standby.

> If not, did you have in mind a CTE with a large VALUES expression, or

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

---

From: Tom Lane <[email-masked]>
Date: Thu, 06 Mar 2025 13:47:34 -0500

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

+1

			regards, tom lane

---

From: Corey Huinker <[email-masked]>
Date: Thu, 6 Mar 2025 13:47:51 -0500

> data, which are obviously *not* stored in memory for the whole run of

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.

---

From: Andres Freund <[email-masked]>
Date: Thu, 6 Mar 2025 14:04:44 -0500

On 2025-03-06 13:47:51 -0500, Corey Huinker wrote:
> 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.

---

From: Tom Lane <[email-masked]>
Date: Thu, 06 Mar 2025 14:08:52 -0500

> 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

---

From: Nathan Bossart <[email-masked]>
Date: Thu, 6 Mar 2025 13:33:13 -0600

> 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

---

From: Andres Freund <[email-masked]>
Date: Thu, 6 Mar 2025 14:34:41 -0500

Hi,

On 2025-03-06 13:47:34 -0500, Tom Lane wrote:
> 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.

> 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

---

From: Tom Lane <[email-masked]>
Date: Thu, 06 Mar 2025 14:51:26 -0500

Andres Freund <[email-masked]> writes:
>> ... I wonder if we could just rip out pg_upgrade's support

> 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

---

From: Jeff Davis <[email-masked]>
Date: Thu, 06 Mar 2025 16:58:37 -0800

> 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

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] rather than batching[2]. 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] 
https://www.postgresql.org/message-id/[email-masked]

[2] https://www.postgresql.org/message-id/[email-masked]

---

From: Jeff Davis <[email-masked]>
Date: Thu, 06 Mar 2025 17:42:30 -0800

> 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

From c47fc9e570ddd083097f4bfc708465cf644f48c2 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email-masked]>
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

---

From: Corey Huinker <[email-masked]>
Date: Thu, 6 Mar 2025 20:47:55 -0500

> it's not terribly important for actual security.

Ok, I'm convinced.

+1

---

From: Corey Huinker <[email-masked]>
Date: Thu, 6 Mar 2025 21:56:52 -0500

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

---

From: Andres Freund <[email-masked]>
Date: Fri, 7 Mar 2025 11:22:40 -0500

Hi,

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

>   --{schema|data|stats}-only

Could we, instead of having --with-$foo, just use --$foo?

Greetings,

Andres Freund

---

From: Jeff Davis <[email-masked]>
Date: Fri, 07 Mar 2025 08:53:29 -0800

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

---

From: Robert Treat <[email-masked]>
Date: Fri, 7 Mar 2025 12:41:08 -0500

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

---

From: Jeff Davis <[email-masked]>
Date: Fri, 07 Mar 2025 10:41:35 -0800

On Fri, 2025-03-07 at 12:41 -0500, Robert Treat wrote:
> 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
> 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

---

From: Tom Lane <[email-masked]>
Date: Fri, 07 Mar 2025 14:14:08 -0500

> 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], 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&dt=2025-03-07%2018%3A19%3A14

---

From: Robert Treat <[email-masked]>
Date: Fri, 7 Mar 2025 15:46:19 -0500

> 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

---

From: Jeff Davis <[email-masked]>
Date: Fri, 07 Mar 2025 13:43:56 -0800

On Fri, 2025-03-07 at 15:46 -0500, Robert Treat wrote:
> 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 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

---

From: Corey Huinker <[email-masked]>
Date: Fri, 7 Mar 2025 22:40:21 -0500

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

> parsed via --include without having to create any new flags.

This is pushing a burden onto our customers for a parsing convenience.

-1.

---

From: Corey Huinker <[email-masked]>
Date: Fri, 7 Mar 2025 22:43:10 -0500

> {schema|data|statistics} for consistency, but that resulted in 9

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.

---

From: Hari Krishna Sunder <[email-masked]>
Date: Fri, 7 Mar 2025 21:51:53 -0800

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

---

From: Corey Huinker <[email-masked]>
Date: Sat, 8 Mar 2025 02:51:23 -0500

> upgrades to higher pg versions.
We can't install functions on the source database - it might be a read
replica.

---

From: Corey Huinker <[email-masked]>
Date: Sat, 8 Mar 2025 02:56:18 -0500

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.
From 9cd4b4e0e280d0fd8cb120ac105d6e65a491cd7e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v7 1/2] Split relation into schemaname and relname.

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

index cede992b6e2..fdd4b8d7dba 100644
@@ -12443,8 +12443,8 @@
@@ -12459,8 +12459,8 @@
 
 # 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 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
 
@@ -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
 
@@ -133,6 +137,9 @@ static void init_empty_stats_tuple(Oid reloid, int16 attnum, bool inherited,
@@ -170,8 +177,23 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 
 	bool		result = true;
 
+	nspoid = stats_schema_check_privileges(nspname);
 
@@ -185,21 +207,18 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
@@ -210,8 +229,8 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
@@ -900,13 +919,33 @@ init_empty_stats_tuple(Oid reloid, int16 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)

 
 	if (attnum < 0)
 		ereport(ERROR,

 
index 52dfa477187..fdc69bc93e2 100644

@@ -32,7 +35,8 @@

@@ -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;
 	}
 
@@ -187,20 +210,22 @@ relation_statistics_update(FunctionCallInfo fcinfo)

 
index 9647f5108b3..e037d4994e8 100644
@@ -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)

 
 
-	free(qualified_name);
 	destroyPQExpBuffer(out);
index 1f46d5e7854..2f1295f2149 100644
@@ -14,7 +14,8 @@ CREATE TABLE stats_import.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);
@@ -84,7 +106,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
@@ -132,7 +155,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
@@ -166,7 +190,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
@@ -187,7 +212,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
@@ -204,7 +230,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
@@ -221,7 +248,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
@@ -238,7 +266,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
@@ -256,7 +285,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
@@ -277,7 +307,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
@@ -295,8 +326,7 @@ WHERE oid = 'stats_import.test'::regclass;
 (1 row)
 
 -- ok: clear stats
  
@@ -313,87 +343,123 @@ WHERE oid = 'stats_import.test'::regclass;
+WARNING:  schema nope does not exist
@@ -421,7 +487,8 @@ AND attname = 'id';
@@ -443,8 +510,9 @@ AND attname = 'id';
 
@@ -467,8 +535,9 @@ AND attname = 'id';
 
@@ -492,8 +561,9 @@ AND attname = 'id';
 
@@ -517,8 +587,9 @@ AND attname = 'id';
 
@@ -544,8 +615,9 @@ AND attname = 'id';
 
@@ -570,8 +642,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
@@ -594,8 +667,9 @@ AND attname = 'id';
 
@@ -619,8 +693,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
@@ -642,8 +717,9 @@ AND attname = 'id';
 
@@ -667,8 +743,9 @@ AND attname = 'tags';
 
@@ -691,8 +768,9 @@ AND attname = 'tags';
 
@@ -718,8 +796,9 @@ AND attname = 'id';
 
@@ -743,8 +822,9 @@ AND attname = 'arange';
 
@@ -768,8 +848,9 @@ AND attname = 'arange';
 
@@ -792,8 +873,9 @@ AND attname = 'arange';
 
@@ -818,8 +900,9 @@ AND attname = 'id';
 
@@ -841,8 +924,9 @@ AND attname = 'arange';
 
@@ -868,8 +952,9 @@ AND attname = 'arange';
 
@@ -895,8 +980,9 @@ AND attname = 'id';
 
@@ -920,8 +1006,9 @@ AND attname = 'tags';
 
@@ -945,8 +1032,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
@@ -969,8 +1057,9 @@ AND attname = 'tags';
 
@@ -1022,8 +1111,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
@@ -1200,9 +1290,10 @@ AND attname = 'arange';
 (1 row)

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

 

@@ -71,7 +87,8 @@ WHERE oid = 'stats_import.test_i'::regclass;
 
 SELECT mode FROM pg_locks
@@ -108,7 +125,8 @@ WHERE oid = 'stats_import.part_parent'::regclass;
 BEGIN;

 SELECT mode FROM pg_locks
@@ -127,7 +145,8 @@ WHERE oid = 'stats_import.part_parent_i'::regclass;
 
@@ -140,7 +159,8 @@ WHERE oid = 'stats_import.test'::regclass;

 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -149,7 +169,8 @@ WHERE oid = 'stats_import.test'::regclass;

 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -158,7 +179,8 @@ WHERE oid = 'stats_import.test'::regclass;

 SELECT relpages, reltuples, relallvisible, relallfrozen
@@ -167,7 +189,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: just relallfrozen
 
@@ -177,7 +200,8 @@ WHERE oid = 'stats_import.test'::regclass;
 
@@ -189,7 +213,8 @@ WHERE oid = 'stats_import.test'::regclass;

@@ -198,8 +223,7 @@ FROM pg_class
 WHERE oid = 'stats_import.test'::regclass;
 
 -- ok: clear stats
 
 SELECT relpages, reltuples, relallvisible
 FROM pg_class
@@ -209,48 +233,70 @@ WHERE oid = 'stats_import.test'::regclass;
 CREATE SEQUENCE stats_import.testseq;

 

 --
 -- attribute stats
 --

--- error: relation null
 
 -- error: NULL attname
 
@@ -258,36 +304,41 @@ SELECT pg_catalog.pg_restore_attribute_stats(

 
 
 -- error: inherited 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,
@@ -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';
 
@@ -336,8 +389,9 @@ AND attname = 'id';
 
@@ -352,8 +406,9 @@ AND attname = 'id';

 
@@ -402,8 +459,9 @@ AND attname = 'id';
 
 -- ok: mcv+mcf
@@ -418,8 +476,9 @@ AND attname = 'id';
 
@@ -434,8 +493,9 @@ AND attname = 'id';
 
 -- ok: histogram_bounds
@@ -449,8 +509,9 @@ AND attname = 'id';
 
@@ -465,8 +526,9 @@ AND attname = 'tags';
 
@@ -481,8 +543,9 @@ AND attname = 'tags';
 
@@ -498,8 +561,9 @@ AND attname = 'id';
 
@@ -514,8 +578,9 @@ AND attname = 'arange';
 
@@ -530,8 +595,9 @@ AND attname = 'arange';
 
@@ -546,8 +612,9 @@ AND attname = 'arange';
 
@@ -562,8 +629,9 @@ AND attname = 'id';

@@ -594,8 +663,9 @@ AND attname = 'arange';
 
@@ -611,8 +681,9 @@ AND attname = 'id';
 
@@ -627,8 +698,9 @@ AND attname = 'tags';
 
@@ -643,8 +715,9 @@ AND attname = 'tags';
 
 -- ok: mcelem
@@ -659,8 +732,9 @@ AND attname = 'tags';
 
@@ -707,8 +781,9 @@ SELECT s.schemaname, s.tablename, s.attname, s.inherited, r.*
@@ -853,9 +928,10 @@ AND inherited = false
 AND attname = 'arange';

 SELECT COUNT(*)
 FROM pg_stats
index 51dd8ad6571..63a260a8ff8 100644
@@ -30348,22 +30348,24 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
@@ -30391,7 +30393,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
@@ -30440,16 +30442,18 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
@@ -30485,7 +30489,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset

base-commit: 21f653cc0024100f8ecc279162631f2b1ba8c46c
-- 
2.48.1

From 4d8d76b78b87f53d0adbd6781a2a66beac5bc264 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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,
 						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,
 						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();
+	}

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

---

From: Robert Treat <[email-masked]>
Date: Sat, 8 Mar 2025 10:56:20 -0500

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

> 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

---

From: Corey Huinker <[email-masked]>
Date: Sat, 8 Mar 2025 14:09:51 -0500

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

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

---

From: Jeff Davis <[email-masked]>
Date: Sun, 09 Mar 2025 10:00:35 -0700

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

---

From: Tom Lane <[email-masked]>
Date: Mon, 10 Mar 2025 17:53:26 -0400

> 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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 10 Mar 2025 16:53:50 -0700

> failure in local testing, and pushed it.

Ah, thank you.

Regards,
	Jeff Davis

---

From: Ashutosh Bapat <[email-masked]>
Date: Tue, 11 Mar 2025 15:50:02 +0530

> > failure in local testing, and pushed it.

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

---

From: Corey Huinker <[email-masked]>
Date: Fri, 14 Mar 2025 16:03:16 -0400

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
From a2c68b8390cf137323f449a4bc826ad66bada0bb Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v8 1/4] Split relation into schemaname and relname.

index 890822eaf79..8dee321d248 100644
@@ -12453,8 +12453,8 @@
@@ -12469,8 +12469,8 @@

 

 

 	bool		result = true;

 

 
 
 	if (attnum < 0)
 		ereport(ERROR,

 

@@ -32,7 +35,8 @@

 

 

 
+
+/*
index c371570501a..bd857bb076c 100644
@@ -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)

 	appendPQExpBuffer(out, "\t'reltuples', '%s'::real,\n", rsinfo->reltuples);
@@ -10602,9 +10602,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 		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);

 

 
 
 -- ok: just relallfrozen

 
 -- ok: clear stats

 

 
 
 -- ok: mcv+mcf

 -- ok: histogram_bounds

 

 

 

 
 
 -- ok: mcelem

  
 
 SELECT

 --
 -- relstats tests
 --

 

 

 

 

 
 
 -- ok: just relallfrozen

 

 -- ok: clear stats

 

 
 --
 -- attribute stats
 --

--- error: relation null
 
 -- error: NULL attname

 

 -- error: inherited null

 

 
 
 -- ok: mcv+mcf

 -- ok: histogram_bounds

 

 

 

 
 
 -- ok: mcelem

 
 SELECT COUNT(*)
 FROM pg_stats
index 1c3810e1a04..a75e95bc5fd 100644
@@ -30365,22 +30365,24 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
@@ -30408,7 +30410,7 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
@@ -30457,16 +30459,18 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
@@ -30502,7 +30506,8 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset

base-commit: 6d376c3b0d1e79c318d2a1c04097025784e28377
-- 
2.48.1

From 98f1eea90be0804ecdd43a89306aa83a6b9784c5 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v8 2/4] Downgrade as man pg_restore_*_stats errors to
 warnings.

 

 

 
 	bool		result = true;

 

 
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 	/*

 

 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 
 
 /*
 
+	/*

 

 
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 

 

 /*
 
 	/*

 

 

 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 
 	/*
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 
 		if (argnulls[i + 1])
 			continue;

 
--- error: relname null

 

 

 

 
--- error: relname null
 
--- error: NULL attname

 

From e2b00e46afdfa45f263b4666831160bf4d21dd09 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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);
 	}
 
+	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);
 
+	if (first_query)
+		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

From 737a29c9b8f146eb037630ab183eb8601a76409a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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

---

From: Corey Huinker <[email-masked]>
Date: Sat, 15 Mar 2025 21:37:51 -0400

> 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.
From 79b459706b09458b4c27d3f80a8fab9ec9600ce7 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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

From 72d0fe4b5de382a2f36151de2b26960d3a85267f Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 14 Mar 2025 01:06:19 -0400
Subject: [PATCH v9 3/5] Introduce CreateStmtPtr.

 

 
 
 /*
- * dumpRelationStats --

 

 

 

 

 
 
 	PQclear(res);

 /*
-- 
2.48.1

From fd2cced89c11353e909e6ef9bb824a3a7536e6cb Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Tue, 4 Mar 2025 22:16:52 -0500
Subject: [PATCH v9 1/5] Split relation into schemaname and relname.

 

 

 
 
 	bool		result = true;

 

 
 
 	if (attnum < 0)
 		ereport(ERROR,

 

@@ -32,7 +35,8 @@

 

 

 
+
+/*

 
 
-	free(qualified_name);
 	destroyPQExpBuffer(out);

 

 
 
 -- ok: just relallfrozen

 
 -- ok: clear stats

 

 
 
 -- ok: mcv+mcf

 -- ok: histogram_bounds

 

 

 

 
 
 -- ok: mcelem

  
 
 SELECT

 --
 -- relstats tests
 --

 

 

 

 

 
 
 -- ok: just relallfrozen

 

 -- ok: clear stats

 

 
 --
 -- attribute stats
 --

--- error: relation null
 
 -- error: NULL attname

 

 -- error: inherited null

 

 
 
 -- ok: mcv+mcf

 -- ok: histogram_bounds

 

 

 

 
 
 -- ok: mcelem

 
 SELECT COUNT(*)
 FROM pg_stats

base-commit: 5eabd91a83adae75f53b61857343660919fef4c7
-- 
2.48.1

From b5b96d4a2fe4119ef26689cc8f3e1a5b8d24bdda Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v9 2/5] Downgrade as man pg_restore_*_stats errors to
 warnings.

 

 

 
 	bool		result = true;

 

 
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 	/*

 

 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 
 
 /*
 
+	/*

 

 
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 

 

 /*
 
 	/*

 

 

 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 
 	/*
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 
 		if (argnulls[i + 1])
 			continue;

 
--- error: relname null

 

 

 

 
--- error: relname null
 
--- error: NULL attname

 

From d9ab97b8ca00d03ab370de5b99546d01e0480bd5 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 14 Mar 2025 03:54:26 -0400
Subject: [PATCH v9 4/5] Batching getAttributeStats().

 

 
+
+

 int
 
 /*
 
-	PQExpBufferData query;
 	PQExpBufferData out;
 
-	PGresult   *res;
-
 
 	initPQExpBuffer(&out);

 
 
-	if (first_query)
+	/*
 
 		/*

-	PQclear(res);
-
 
-- 
2.48.1

---

From: Nathan Bossart <[email-masked]>
Date: Sun, 16 Mar 2025 15:33:49 -0500

Thanks for working on this, Corey.

> is consumed. It seems to have obvious speedups.

I've been doing a variety of tests with my toy database of 100K relations
[0], and I'm seeing around 20% less memory usage.  That's still 20% more
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] https://postgr.es/m/Z9R9-mFbxukqKmg4%40nathan

-- 
nathan

---

From: Corey Huinker <[email-masked]>
Date: Sun, 16 Mar 2025 17:32:15 -0400

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

---

From: Nathan Bossart <[email-masked]>
Date: Mon, 17 Mar 2025 09:23:56 -0500

> 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

---

From: Corey Huinker <[email-masked]>
Date: Mon, 17 Mar 2025 19:24:46 -0400

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

---

From: Nathan Bossart <[email-masked]>
Date: Mon, 17 Mar 2025 20:01:48 -0500

On Mon, Mar 17, 2025 at 07:24:46PM -0400, Corey Huinker wrote:
> 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

---

From: Jeff Davis <[email-masked]>
Date: Wed, 19 Mar 2025 15:17:40 -0700

> > 0001 - no changes, but the longer I go the more I'm certain this is

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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 19 Mar 2025 18:35:52 -0400

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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Mar 2025 22:32:12 -0700

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

---

From: Jeff Davis <[email-masked]>
Date: Mon, 24 Mar 2025 23:53:33 -0700

> LookupExplicitNamespace().

Attached new version 9j:

* Changed to use LookupExplicitNamespace()
* Added test for temp tables
* Doc fixes

Regards,
	Jeff Davis

From 72d4b9fc128e6d4ef73bb24ebba41797d06a7d9e Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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 <[email-masked]>
Discussion: https://postgr.es/m/CADkLM=ceOSsx_=[email-masked]
---
 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 @@

 

 	bool		result = true;

 

 
 
 	if (attnum < 0)
 		ereport(ERROR,

 

@@ -32,7 +35,8 @@

 

 
 
index 9647f5108b3..b444f6871df 100644

+
+/*
+ * 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)

@@ -10610,9 +10610,10 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
 
 		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
index 0d29ef50ff2..3f7b82e02bb 100644
 
index 0eb4decfcac..ba09b431c11 100644

index 1f46d5e7854..302e77743e3 100644

 

 
 
 -- ok: just relallfrozen

 
 -- ok: clear stats

 

 
 
 -- ok: mcv+mcf

 -- ok: histogram_bounds

 

 

 

 
 
 -- ok: mcelem

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

 --
 -- relstats tests
 --

 

 

 

 

 
 
 -- ok: just relallfrozen

 

 -- ok: clear stats

 

 
 --
 -- attribute stats
 --

--- error: relation null
 
 -- error: NULL attname

 

 -- error: inherited null

 

 
 
 -- ok: mcv+mcf

 -- ok: histogram_bounds

 

 

 

 
 
 -- ok: mcelem

 
 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

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Mar 2025 10:53:22 -0400

> * Changed to use LookupExplicitNamespace()

Seems good.

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

---

From: Robert Treat <[email-masked]>
Date: Tue, 25 Mar 2025 13:51:17 -0400

> 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

---

From: Jeff Davis <[email-masked]>
Date: Tue, 25 Mar 2025 11:42:22 -0700

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

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

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Mar 2025 15:59:13 -0400

+1

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

---

From: Corey Huinker <[email-masked]>
Date: Tue, 25 Mar 2025 21:41:25 -0400

> 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.
From 1f9b2578f55fa1233121bcf5949a6f69d6cf8cee Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 14 Mar 2025 03:54:26 -0400
Subject: [PATCH v10 2/4] Batching getAttributeStats().

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

index 224dc8c9330..e3f2dac33ec 100644

 
@@ -222,6 +253,8 @@ static int	nsequences = 0;
 
+
+
 /*
@@ -399,6 +432,9 @@ static void setupDumpWorker(Archive *AH);

 int
@@ -10520,7 +10556,286 @@ statisticsDumpSection(const RelStatsInfo *rsinfo)
 }
 
 /*
@@ -10528,64 +10843,21 @@ static char *
 
-	PQExpBufferData query;
 	PQExpBufferData out;
 
-	PGresult   *res;
-
 
 	initPQExpBuffer(&out);
 
@@ -10604,132 +10876,72 @@ printRelationStats(Archive *fout, const void *userArg)
 	appendPQExpBuffer(&out, "\t'relallvisible', '%d'::integer\n);\n",
 					  rsinfo->relallvisible);

 
-	if (first_query)
+	/*
 
 		/*

-	PQclear(res);
-
 
-- 
2.49.0

From 651e70ae705d5a4f081509e66a743422d2e86ae4 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
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(-)

index 512eb776e0e..809c8263a41 100644

 
 extern Oid	stats_lookup_relid(const char *nspname, const char *relname);
 
index f5eb17ba42d..b7ba1622391 100644
 
@@ -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;

 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		return false;

 
 							attname, relname)));
@@ -216,27 +232,33 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 					 errmsg("column %d of relation \"%s\" does not exist",
 							attnum, relname)));
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 	/*
@@ -285,10 +307,11 @@ attribute_statistics_update(FunctionCallInfo fcinfo)
 	}

 	/* 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,

 
 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

@@ -645,6 +676,7 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum,
 		*atttypcoll = DEFAULT_COLLATION_OID;

 /*
@@ -770,6 +802,10 @@ set_stats_slot(Datum *values, bool *nulls, bool *replaces,
 	if (slotidx >= STATISTIC_NUM_SLOTS && first_empty >= 0)
 		slotidx = first_empty;
 
+	/*
@@ -915,38 +951,54 @@ pg_clear_attribute_stats(PG_FUNCTION_ARGS)
 	AttrNumber	attnum;
 	bool		inherited;

 
 	reloid = stats_lookup_relid(nspname, relname);
+	if (!OidIsValid(reloid))
+		PG_RETURN_VOID();

 
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 
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;

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

index a9a3224efe6..d587e875457 100644
@@ -33,16 +33,20 @@
 
 /*
@@ -127,13 +131,14 @@ stats_check_arg_pair(FunctionCallInfo fcinfo,
 
 	/*
@@ -173,14 +178,15 @@ stats_lock_check_privileges(Oid reloid)

@@ -193,25 +199,33 @@ stats_lock_check_privileges(Oid reloid)
 		relation_close(index, NoLock);
 	}

 

 /*
@@ -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,
 
 	/*
 	 * 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,
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 
 		if (argnulls[i + 1])
 			continue;
index 48d6392b4ad..161cf67b711 100644
@@ -46,49 +46,85 @@ SELECT pg_clear_relation_stats('stats_import', 'test');
+WARNING:  relation "stats_import.nope" does not exist
@@ -340,65 +376,110 @@ CREATE SEQUENCE stats_import.testseq;
-ERROR:  schema "nope" does not exist
+WARNING:  relation "nope.test" does not exist
+WARNING:  relation "stats_import.nope" does not exist
@@ -407,8 +488,13 @@ SELECT pg_catalog.pg_restore_attribute_stats(
+WARNING:  column "nope" of relation "test" does not exist
@@ -416,30 +502,50 @@ SELECT pg_catalog.pg_restore_attribute_stats(
index d140733a750..be8045ceea5 100644

 

 

 

 
--- error: relname null
 
--- error: NULL attname

 

2.49.0

From 8611beb5a7906d0f7e93fb68aa41dd58bc7ab80f Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 14 Mar 2025 01:06:19 -0400
Subject: [PATCH v10 1/4] Introduce CreateStmtPtr.

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

index 82d51c89ac6..e512201ed58 100644
@@ -1264,6 +1264,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 
@@ -2620,7 +2623,17 @@ WriteToc(ArchiveHandle *AH)
@@ -3856,6 +3869,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 
index e41e645f649..224dc8c9330 100644
@@ -10520,51 +10520,44 @@ statisticsDumpSection(const RelStatsInfo *rsinfo)
 }
 
 /*
- * dumpRelationStats --

@@ -10573,82 +10566,85 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)

 

 

 

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

 	PQclear(res);

 /*

base-commit: bde2fb797aaebcbe06bf60f330ba5a068f17dda7
-- 
2.49.0

From 87da7d4c517c3b2e63666892e64b9de2a8dbbe44 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 15 Mar 2025 17:34:30 -0400
Subject: [PATCH v10 3/4] Add relallfrozen to pg_dump statistics.

index e3f2dac33ec..6c366fd55d3 100644
@@ -6897,7 +6897,8 @@ getFuncs(Archive *fout)
@@ -6926,6 +6927,7 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
@@ -6965,6 +6967,7 @@ getTables(Archive *fout, int *numTables)
@@ -7015,8 +7018,13 @@ getTables(Archive *fout, int *numTables)
@@ -7181,6 +7189,7 @@ getTables(Archive *fout, int *numTables)
@@ -7228,6 +7237,7 @@ getTables(Archive *fout, int *numTables)
 
@@ -7330,7 +7340,7 @@ getTables(Archive *fout, int *numTables)
 
 		/*
@@ -7599,6 +7609,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7653,7 +7664,12 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7769,6 +7785,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7840,6 +7857,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 			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)

 			contype = *(PQgetvalue(res, j, i_contype));
@@ -10862,19 +10880,21 @@ printRelationStats(Archive *fout, const void *userArg)
 	initPQExpBuffer(&out);

 	AH->txnCount++;

 	/*
index 51ebf8ad13c..576326daec7 100644
@@ -4771,7 +4771,8 @@ my %tests = (
2.49.0

---

From: Corey Huinker <[email-masked]>
Date: Fri, 28 Mar 2025 21:11:05 -0400

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.
From 96b10b1eb955c5619d23cadf7de8b12d2db638a9 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 15 Mar 2025 17:34:30 -0400
Subject: [PATCH v11 1/4] Add relallfrozen to pg_dump statistics.

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

index 84a78625820..211cf10dbd6 100644
@@ -6874,7 +6874,8 @@ getFuncs(Archive *fout)
@@ -6903,6 +6904,7 @@ getRelationStatistics(Archive *fout, DumpableObject *rel, int32 relpages,
@@ -6967,6 +6969,7 @@ getTables(Archive *fout, int *numTables)
@@ -7017,8 +7020,13 @@ getTables(Archive *fout, int *numTables)
@@ -7183,6 +7191,7 @@ getTables(Archive *fout, int *numTables)
@@ -7230,6 +7239,7 @@ getTables(Archive *fout, int *numTables)
 
 		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)

 			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 */
 
 	/*

base-commit: a0a4601765b896079eb82a9d5cfa1f41154fcfdb
-- 
2.49.0

From fd6cd3691e21b807a299749631dc0bdddc886853 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v11 4/4] Downgrade many pg_restore_*_stats errors to warnings.

 

 

 
 	bool		result = true;

 

 
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 	/*

 

 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 
 
 /*
 
+	/*

 

 
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 

 

 
 
 /*
 
 	/*

 

 
 
 /*

 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 
 	/*
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 
 		if (argnulls[i + 1])
 			continue;

 

 

 

 
--- error: relname null
 
--- error: NULL attname

 

From 7b226732d1e68b5899c3dc8fbc6eb940c0f884ad Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 14 Mar 2025 03:54:26 -0400
Subject: [PATCH v11 3/4] Batching getAttributeStats().

index b7571ea15eb..bcf9dd1eb47 100644

 
 
+
+
 /*

 int
@@ -10556,7 +10592,286 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
@@ -10564,64 +10879,21 @@ static char *
 
-	PQExpBufferData query;
 	PQExpBufferData out;
 
-	PGresult   *res;
-
 
 	initPQExpBuffer(&out);
 
@@ -10642,132 +10914,72 @@ printRelationStats(Archive *fout, const void *userArg)
 	appendPQExpBufferStr(&out, "\n);\n");

 
-	if (first_query)
+	/*
 
 		/*

-	PQclear(res);
-
 
-- 
2.49.0

From c71426ee2068d37237b79a65d1a0764b7cd3d60f Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 14 Mar 2025 01:06:19 -0400
Subject: [PATCH v11 2/4] Introduce CreateStmtPtr.

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

 
index 1d131e5a57d..1b4c62fd7d7 100644
@@ -1265,6 +1265,9 @@ ArchiveEntry(Archive *AHX, CatalogId catalogId, DumpId dumpId,
 
@@ -2621,7 +2624,17 @@ WriteToc(ArchiveHandle *AH)
@@ -3877,6 +3890,13 @@ _printTocEntry(ArchiveHandle *AH, TocEntry *te, const char *pfx)
 
index 211cf10dbd6..b7571ea15eb 100644
@@ -10556,42 +10556,44 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --

@@ -10600,88 +10602,87 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)

 

 
-	appendPQExpBuffer(out, "\t'relallvisible', '%d'::integer",
+	appendPQExpBuffer(&out, "\n\t'version', '%u'::integer", fout->remoteVersion);
 
 	if (fout->remoteVersion >= 180000)
-		appendPQExpBuffer(out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);
+		appendPQExpBuffer(&out, ",\n\t'relallfrozen', '%d'::integer", rsinfo->relallfrozen);

 

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

 	PQclear(res);
 
 							  .section = rsinfo->section,
-							  .deps = dobj->dependencies,
-							  .nDeps = dobj->nDeps));
+							  .deps = deps,
+							  .nDeps = ndeps));
 }
 
 /*
-- 
2.49.0

---

From: Jeff Davis <[email-masked]>
Date: Fri, 28 Mar 2025 22:29:16 -0700

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

From 154b8b5c10ec330c26ccd9006c434a7db1feef04 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 15 Mar 2025 17:34:30 -0400
Subject: [PATCH v11j] Add relallfrozen to pg_dump statistics.

Author: Corey Huinker <[email-masked]>
Discussion: https://postgr.es/m/CADkLM=[email-masked]
---
 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(-)

index 84a78625820..4ca34be230c 100644
@@ -7017,8 +7020,15 @@ getTables(Archive *fout, int *numTables)
+		appendPQExpBufferStr(query, "0 AS relallfrozen, ");
@@ -7183,6 +7193,7 @@ getTables(Archive *fout, int *numTables)
@@ -7230,6 +7241,7 @@ getTables(Archive *fout, int *numTables)
 
@@ -7336,7 +7348,7 @@ getTables(Archive *fout, int *numTables)
@@ -7609,6 +7621,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7663,7 +7676,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
+		appendPQExpBufferStr(query, "0 AS relallfrozen, ");
@@ -7779,6 +7799,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
@@ -7850,6 +7871,7 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 
 			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)

@@ -10618,9 +10640,15 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)

 	/*
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

---

From: Corey Huinker <[email-masked]>
Date: Sat, 29 Mar 2025 01:44:57 -0400

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

> to pg_restore_relation_stats. We might need a filtering step in

That sounds trickier. Do we already have filtering steps that are sensitive
to the "before" version dump?

---

From: Corey Huinker <[email-masked]>
Date: Mon, 31 Mar 2025 11:11:47 -0400

> That sounds trickier.

Narrator: It was not trickier.

In light of v11-0001 being committed as 4694aedf63bf, I've rebased the
remaining patches.
From 607984bdcc91fa31fb7a12e9b24fb8704aa14975 Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 14 Mar 2025 01:06:19 -0400
Subject: [PATCH v12 1/3] Introduce CreateStmtPtr.

 

 
index 4ca34be230c..cc195d6cd9e 100644
@@ -10560,42 +10560,44 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
- * dumpRelationStats --

@@ -10604,88 +10606,87 @@ dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)

 

 

 

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

 	PQclear(res);

 /*

base-commit: e2809e3a1015697832ee4d37b75ba1cd0caac0f0
-- 
2.49.0

From 410171805037718c3adcae778bba56c485038e3f Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Fri, 14 Mar 2025 03:54:26 -0400
Subject: [PATCH v12 2/3] Batching getAttributeStats().

index cc195d6cd9e..26144371b1b 100644

 
 
+
+
 /*

 int
@@ -10560,7 +10596,286 @@ appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
 }
 
 /*
@@ -10568,64 +10883,21 @@ static char *
 
-	PQExpBufferData query;
 	PQExpBufferData out;
 
-	PGresult   *res;
-
 
 	initPQExpBuffer(&out);
 
@@ -10646,132 +10918,72 @@ printRelationStats(Archive *fout, const void *userArg)
 	appendPQExpBufferStr(&out, "\n);\n");

 
-	if (first_query)
+	/*
 
 		/*

-	PQclear(res);
-
 
-- 
2.49.0

From 16794820dedd79ec58f8692da5b50a4d8976620a Mon Sep 17 00:00:00 2001
From: Corey Huinker <[email-masked]>
Date: Sat, 8 Mar 2025 00:52:41 -0500
Subject: [PATCH v12 3/3] Downgrade many pg_restore_*_stats errors to warnings.

 

 

 
 	bool		result = true;

 

 
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 	/*

 

 	if (attr->attisdropped)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 
 
 /*
 
+	/*

 

 
 
 	if (attnum < 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,

 

 

 
 
 /*
 
 	/*

 

 
 
 /*

 	if (nargs % 2 != 0)
-		ereport(ERROR,
+	{
+		ereport(WARNING,
 
 	/*
 
 		if (argnulls[i])
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 
 		if (types[i] != TEXTOID)
-			ereport(ERROR,
+		{
+			ereport(WARNING,
 
 		if (argnulls[i + 1])
 			continue;

 

 

 

 
--- error: relname null
 
--- error: NULL attname

---

From: Robert Haas <[email-masked]>
Date: Mon, 31 Mar 2025 13:39:41 -0400

On Thu, Feb 27, 2025 at 10:43 PM Greg Sabino Mullane <[email-masked]> 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.

---

From: Jeff Davis <[email-masked]>
Date: Mon, 31 Mar 2025 15:04:05 -0700

> pg_upgrade to preserve them, but I think doing so for a regular dump

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

---

From: Nathan Bossart <[email-masked]>
Date: Mon, 31 Mar 2025 21:33:15 -0500

> In light of v11-0001 being committed as 4694aedf63bf, I've rebased the

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

---

From: Robert Treat <[email-masked]>
Date: Mon, 31 Mar 2025 23:02:49 -0400

> 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

---

From: Robert Haas <[email-masked]>
Date: Tue, 1 Apr 2025 09:37:30 -0400

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

---

From: Nathan Bossart <[email-masked]>
Date: Tue, 1 Apr 2025 13:20:30 -0500

> that is subject to change based on any feedback or objections that emerge

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

---

From: Nathan Bossart <[email-masked]>
Date: Tue, 1 Apr 2025 13:44:14 -0500

> be no functional differences, but I did restructure 0003 to make it even

Apologies for the noise.  I noticed one more way to simplify 0002.  As
before, there should be no functional differences.

-- 
nathan

---

From: Jeff Davis <[email-masked]>
Date: Tue, 01 Apr 2025 13:24:14 -0700

On Tue, 2025-04-01 at 09:37 -0400, Robert Haas wrote:
> 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

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

---

From: Robert Haas <[email-masked]>
Date: Tue, 1 Apr 2025 22:24:25 -0400

> I lean towards making it opt-in for pg_dump and opt-out for pg_upgrade.

Big +1.

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

> and give people the chance to respond to a top-level thread?

wfm, thanks.

---

From: Nathan Bossart <[email-masked]>
Date: Tue, 1 Apr 2025 22:21:44 -0500

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

---

From: Jeff Davis <[email-masked]>
Date: Tue, 01 Apr 2025 22:44:19 -0700

On Tue, 2025-04-01 at 22:21 -0500, Nathan Bossart wrote:
> that many bytes the second time round.

That feels like a better approach.

>   That's simple enough, but it relies
> 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.

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

> 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

---

From: Andres Freund <[email-masked]>
Date: Wed, 2 Apr 2025 12:42:01 -0400

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

---

From: Nathan Bossart <[email-masked]>
Date: Wed, 2 Apr 2025 21:26:32 -0500

> extra callbacks get too messy, we can just document the assumptions

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.

> 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], 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] https://postgr.es/m/20180727015306.fzlo4inv5i3zqr2c%40alap3.anarazel.de

-- 
nathan

---

From: Nathan Bossart <[email-masked]>
Date: Wed, 2 Apr 2025 21:38:18 -0500

On Wed, Apr 02, 2025 at 10:34:58PM -0400, Corey Huinker wrote:
> In my patch, that SQL statement came with the comment:
> 
> + /*

Thanks.  I included that in the latest patch set.

-- 
nathan

---

From: Nathan Bossart <[email-masked]>
Date: Thu, 3 Apr 2025 21:19:51 -0500

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

---

From: Nathan Bossart <[email-masked]>
Date: Fri, 4 Apr 2025 14:56:54 -0500

> Great.  I'm planning to commit the attached patch set tomorrow morning.

Committed.

-- 
nathan

---

From: Nathan Bossart <[email-masked]>
Date: Fri, 4 Apr 2025 15:06:45 -0500

> Committed.

I see the buildfarm failure and am working on a fix.

-- 
nathan

---

From: Nathan Bossart <[email-masked]>
Date: Fri, 4 Apr 2025 15:58:53 -0500

> I see the buildfarm failure and am working on a fix.

I pushed commit 8ec0aae to fix this.

-- 
nathan

---

From: Nathan Bossart <[email-masked]>
Date: Fri, 4 Apr 2025 17:25:29 -0500

> 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

---

From: Nathan Bossart <[email-masked]>
Date: Fri, 4 Apr 2025 21:06:38 -0500

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

---

From: Greg Sabino Mullane <[email-masked]>
Date: Sat, 10 May 2025 15:51:05 -0400

> > I lean towards making it opt-in for pg_dump and opt-out for pg_upgrade.

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

---

From: Hari Krishna Sunder <[email-masked]>
Date: Tue, 13 May 2025 17:01:02 -0700

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.

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.

---

From: Nathan Bossart <[email-masked]>
Date: Wed, 14 May 2025 11:53:08 -0400

> but the perf of these tables immediately after the upgrade is considerably

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.

> 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

---

From: Hari Krishna Sunder <[email-masked]>
Date: Wed, 14 May 2025 13:30:48 -0700

Thanks Nathan.
Here is the patch with a comment.

On Wed, May 14, 2025 at 8:53 AM Nathan Bossart <[email-masked]>
wrote:

> > <
> https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=3d351d916b20534f973eda760cde17d96545d4c4
> >
> > changed

---

From: Hari Krishna Sunder <[email-masked]>
Date: Fri, 16 May 2025 11:47:12 -0700

Gentle ping on this.

---
Hari Krishna Sunder

---

From: Michael Paquier <[email-masked]>
Date: Mon, 19 May 2025 10:52:15 +0900

> 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

---

From: Nathan Bossart <[email-masked]>
Date: Mon, 19 May 2025 11:51:32 -0500

> Here is the patch with a comment.

Thanks.

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

---

From: Hari Krishna Sunder <[email-masked]>
Date: Mon, 19 May 2025 14:13:45 -0700

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 <[email-masked]>
wrote:

> > On Wed, May 14, 2025 at 8:53 AM Nathan Bossart <[email-masked]
> >
> > wrote:

---

From: Nathan Bossart <[email-masked]>
Date: Mon, 19 May 2025 16:31:57 -0500

On Mon, May 19, 2025 at 02:13:45PM -0700, Hari Krishna Sunder wrote:
> 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

---

From: Hari Krishna Sunder <[email-masked]>
Date: Tue, 20 May 2025 10:32:39 -0700

Ah ya, forgot that reltuples are not always accurate. This sounds
reasonable to me.

---

From: Nathan Bossart <[email-masked]>
Date: Wed, 21 May 2025 11:08:23 -0500

> Ah ya, forgot that reltuples are not always accurate. This sounds

Cool.  Here is what I have staged for commit, which I am planning to do
shortly.

-- 
nathan

---

From: Hari Krishna Sunder <[email-masked]>
Date: Wed, 21 May 2025 12:33:03 -0700

Looks good to me.

---

From: Jeff Davis <[email-masked]>
Date: Wed, 21 May 2025 14:14:55 -0700

> Cool.  Here is what I have staged for commit, which I am planning to

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

---

From: Nathan Bossart <[email-masked]>
Date: Wed, 21 May 2025 16:29:56 -0500

> 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

---

From: Corey Huinker <[email-masked]>
Date: Wed, 21 May 2025 19:11:09 -0400

> import.  But I see no particular benefit from moving the complexity to the

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

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

---

From: Jeff Davis <[email-masked]>
Date: Wed, 21 May 2025 16:53:17 -0700

On Wed, 2025-05-21 at 16:29 -0500, Nathan Bossart wrote:
> 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

---

From: Robert Haas <[email-masked]>
Date: Thu, 22 May 2025 10:20:16 -0400

On Sat, May 10, 2025 at 3:51 PM Greg Sabino Mullane <[email-masked]> 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.
>
> 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?

---

From: Nathan Bossart <[email-masked]>
Date: Thu, 22 May 2025 09:30:40 -0500

On Thu, May 22, 2025 at 10:20:16AM -0400, Robert Haas wrote:
> It also sort

+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

---

From: Tom Lane <[email-masked]>
Date: Thu, 22 May 2025 10:53:12 -0400

> happy to pick it up in the next week or so.

Works for me, too.

			regards, tom lane

---

From: Nathan Bossart <[email-masked]>
Date: Thu, 22 May 2025 10:25:03 -0500

> differences are fixed up during import?

WFM.  I've committed the patch.

-- 
nathan

---

From: Jeff Davis <[email-masked]>
Date: Thu, 22 May 2025 11:52:21 -0700

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

From b76cb91441e2eefe278249e23fcd703d27a85a06 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email-masked]>
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=[email-masked]
---
 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

---

From: Hari Krishna Sunder <[email-masked]>
Date: Thu, 22 May 2025 12:25:29 -0700

Thanks for the help. This has unblocked us!

---

From: Greg Sabino Mullane <[email-masked]>
Date: Thu, 22 May 2025 15:29:38 -0400

On Thu, May 22, 2025 at 2:52 PM Jeff Davis <[email-masked]> 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
> 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