Per offline conversation with Jeff, adding a --no-schema to pg_dump option
both for completeness (we already have --no-data and --no-statistics), but
users who previously got the effect of --no-schema did so by specifying
--data-only, which suppresses statistics as well.
0001-0005 - changes to pg_dump/pg_upgrade
0006 - attribute stats optimization
0007-0012 - vacuumdb



On Wed, Dec 11, 2024 at 10:49 PM Corey Huinker <corey.huin...@gmail.com>
wrote:

> +1, assuming such an option is wanted at all. I suppose it should be
>> there for the unlikely (and hopefully impossible) case that statistics
>> are causing a problem during upgrade.
>>
>
> Here you go, rebased and re-ordered:
>
> 0001-0004 are the pg_dump/pg_upgrade related patches.
> 0005 is an optimization to the attribute stats update
> 0006-0011 is the still-up-for-debate vacuumdb changes.
>
> The patch for handling the as-yet-theoretical change to default relpages
> for partitioned tables got messy in the rebase, so I decided to just leave
> it out for now, as the change to relpages looks increasingly unlikely.
>
>
>
From 27674ebea83387e0d714239effac486a30d8d70d Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Thu, 12 Dec 2024 16:58:21 -0500
Subject: [PATCH v35 04/12] Add --no-schema option to pg_dump, etc.

Previously, users could use --data-only when they wanted to suppress
schema from a dump. However, that no longer makes sense now that the
data/schema binary has become the data/schema/statistics trinary.
---
 src/bin/pg_dump/pg_backup.h          |  2 ++
 src/bin/pg_dump/pg_backup_archiver.c |  1 +
 src/bin/pg_dump/pg_dump.c            |  6 ++++--
 src/bin/pg_dump/pg_restore.c         |  6 +++++-
 doc/src/sgml/ref/pg_dump.sgml        |  9 +++++++++
 doc/src/sgml/ref/pg_restore.sgml     | 10 ++++++++++
 6 files changed, 31 insertions(+), 3 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 241855d017..24295110cf 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -112,6 +112,7 @@ typedef struct _restoreOptions
 	int			no_comments;	/* Skip comments */
 	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
+	int			no_schema;			/* Skip schema generation */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
 	int			no_statistics;		/* Skip statistics import */
@@ -187,6 +188,7 @@ typedef struct _dumpOptions
 	int			no_subscriptions;
 	int			no_statistics;
 	int			no_data;
+	int			no_schema;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 41001e64ac..d62f419560 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -187,6 +187,7 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
 	dopt->no_data = ropt->no_data;
+	dopt->no_schema = ropt->no_schema;
 	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index e626243f3a..61f90d7f78 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -497,6 +497,7 @@ main(int argc, char **argv)
 		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-schema", no_argument, &dopt.no_schema, 1},
 		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
@@ -799,7 +800,8 @@ main(int argc, char **argv)
 
 	if (data_only && dopt.no_data)
 		pg_fatal("options -a/--data-only and --no-data cannot be used together");
-
+	if (schema_only && dopt.no_schema)
+		pg_fatal("options -s/--schema-only and --no-schema cannot be used together");
 	if (statistics_only && dopt.no_statistics)
 		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
@@ -817,7 +819,7 @@ main(int argc, char **argv)
 
 	/* set derivative flags */
 	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
-	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only && !dopt.no_schema);
 	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 31c3cd32de..90db03f0d4 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -72,6 +72,7 @@ main(int argc, char **argv)
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
 	static int	no_data = 0;
+	static int	no_schema = 0;
 	static int	no_comments = 0;
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
@@ -134,6 +135,7 @@ main(int argc, char **argv)
 		{"filter", required_argument, NULL, 4},
 		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-data", no_argument, &no_data, 1},
+		{"no-schema", no_argument, &no_schema, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -376,7 +378,7 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only && !statistics_only);
+	opts->dumpSchema = (!no_schema && !data_only && !statistics_only);
 	opts->dumpData = (!no_data && !schema_only && !statistics_only);
 	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
@@ -511,7 +513,9 @@ usage(const char *progname)
 	printf(_("  --no-data-for-failed-tables  do not restore data of tables that could not be\n"
 			 "                               created\n"));
 	printf(_("  --no-publications            do not restore publications\n"));
+	printf(_("  --no-schema                  do not restore schema\n"));
 	printf(_("  --no-security-labels         do not restore security labels\n"));
+	printf(_("  --no-statistics              do not restore statistics\n"));
 	printf(_("  --no-subscriptions           do not restore subscriptions\n"));
 	printf(_("  --no-table-access-method     do not restore table access methods\n"));
 	printf(_("  --no-tablespaces             do not restore tablespace assignments\n"));
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index aa4785c612..889b8d4e18 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -1113,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not dump schema.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-statistics</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index ff1441a243..cc9dbb4808 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -738,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-schema</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore schema, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-statistics</option></term>
       <listitem>
-- 
2.47.1

From a84983e60b37693b02362e1dacfda1bcca92dd91 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Sat, 16 Mar 2024 17:21:10 -0400
Subject: [PATCH v35 01/12] Enable dumping of table/index stats in pg_dump.

For each table/matview/index dumped, it will generate a statement that
calls pg_set_relation_stats(), and it will generate a series of
statements that call pg_set_attribute_stats(), one per attribute. These
statements will restore the statistics of the current system onto the
destination system.

Adds the command-line options -X / --statistics-only, which are mutually
exclusive to --schema-only and --data-only.

Statistics are not dumped when --schema-only is specified, except during
a binary upgrade.

As is the pattern with pg_dump options, staistics can be disabled using
--no-statistics.

Table statistics are dumped in the data section. This is true even if
dumping stats in a binary upgrade. Index and Materialized View
statistics are dumped in the post-data section.
---
 src/bin/pg_dump/pg_backup.h          |   4 +
 src/bin/pg_dump/pg_backup_archiver.c |   5 +
 src/bin/pg_dump/pg_dump.c            | 375 ++++++++++++++++++++++++++-
 src/bin/pg_dump/pg_dump.h            |   8 +
 src/bin/pg_dump/pg_dump_sort.c       |   5 +
 src/bin/pg_dump/pg_dumpall.c         |   5 +
 src/bin/pg_dump/pg_restore.c         |  18 +-
 src/bin/pg_dump/t/001_basic.pl       |  18 ++
 doc/src/sgml/ref/pg_dump.sgml        |  36 ++-
 doc/src/sgml/ref/pg_restore.sgml     |  31 ++-
 10 files changed, 489 insertions(+), 16 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index f0f19bb0b2..8fbb39d399 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -113,6 +113,7 @@ typedef struct _restoreOptions
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
+	int			no_statistics;		/* Skip statistics import */
 	int			strict_names;
 
 	const char *filename;
@@ -160,6 +161,7 @@ typedef struct _restoreOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } RestoreOptions;
 
 typedef struct _dumpOptions
@@ -182,6 +184,7 @@ typedef struct _dumpOptions
 	int			no_security_labels;
 	int			no_publications;
 	int			no_subscriptions;
+	int			no_statistics;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
@@ -208,6 +211,7 @@ typedef struct _dumpOptions
 	/* flags derived from the user-settable flags */
 	bool		dumpSchema;
 	bool		dumpData;
+	bool		dumpStatistics;
 } DumpOptions;
 
 /*
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 707a3fc844..185d7fbb7e 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -2962,6 +2962,10 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	if (ropt->no_subscriptions && strcmp(te->desc, "SUBSCRIPTION") == 0)
 		return 0;
 
+	/* If it's a stats dump, maybe ignore it */
+	if (ropt->no_statistics && strcmp(te->desc, "STATISTICS") == 0)
+		return 0;
+
 	/* Ignore it if section is not to be dumped/restored */
 	switch (curSection)
 	{
@@ -2991,6 +2995,7 @@ _tocEntryRequired(TocEntry *te, teSection curSection, ArchiveHandle *AH)
 	 */
 	if (strcmp(te->desc, "ACL") == 0 ||
 		strcmp(te->desc, "COMMENT") == 0 ||
+		strcmp(te->desc, "STATISTICS") == 0 ||
 		strcmp(te->desc, "SECURITY LABEL") == 0)
 	{
 		/* Database properties react to createDB, not selectivity options. */
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 89276524ae..8f85b04c33 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -430,6 +430,7 @@ main(int argc, char **argv)
 	DataDirSyncMethod sync_method = DATA_DIR_SYNC_METHOD_FSYNC;
 	bool		data_only = false;
 	bool		schema_only = false;
+	bool		statistics_only = false;
 
 	static DumpOptions dopt;
 
@@ -466,6 +467,7 @@ main(int argc, char **argv)
 		{"encoding", required_argument, NULL, 'E'},
 		{"help", no_argument, NULL, '?'},
 		{"version", no_argument, NULL, 'V'},
+		{"statistics-only", no_argument, NULL, 'X'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -494,6 +496,7 @@ main(int argc, char **argv)
 		{"no-comments", no_argument, &dopt.no_comments, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
+		{"no-statistics", no_argument, &dopt.no_statistics, 1},
 		{"no-subscriptions", no_argument, &dopt.no_subscriptions, 1},
 		{"no-toast-compression", no_argument, &dopt.no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &dopt.no_unlogged_table_data, 1},
@@ -539,7 +542,7 @@ main(int argc, char **argv)
 
 	InitDumpOptions(&dopt);
 
-	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:RsS:t:T:U:vwWxZ:",
+	while ((c = getopt_long(argc, argv, "abBcCd:e:E:f:F:h:j:n:N:Op:PRsS:t:T:U:vwWxXZ:",
 							long_options, &optindex)) != -1)
 	{
 		switch (c)
@@ -613,6 +616,10 @@ main(int argc, char **argv)
 				dopt.cparams.pgport = pg_strdup(optarg);
 				break;
 
+			case 'X':			/* Dump statistics only */
+				statistics_only = true;
+				break;
+
 			case 'R':
 				/* no-op, still accepted for backwards compatibility */
 				break;
@@ -784,6 +791,13 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+
+	if (statistics_only && dopt.no_statistics)
+		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
 	if (schema_only && foreign_servers_include_patterns.head != NULL)
 		pg_fatal("options -s/--schema-only and --include-foreign-data cannot be used together");
@@ -798,8 +812,20 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpSchema = (!data_only);
-	dopt.dumpData = (!schema_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
+
+	if (statistics_only)
+		/* stats are only thing wanted */
+		dopt.dumpStatistics = true;
+	else if (dopt.no_statistics)
+		/* stats specifically excluded */
+		dopt.dumpStatistics = false;
+	else if (dopt.binary_upgrade)
+		/* binary upgrade and not specifically excluded */
+		dopt.dumpStatistics = true;
+	else
+		dopt.dumpStatistics = (!data_only && !schema_only);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
@@ -1099,6 +1125,7 @@ main(int argc, char **argv)
 	ropt->dropSchema = dopt.outputClean;
 	ropt->dumpData = dopt.dumpData;
 	ropt->dumpSchema = dopt.dumpSchema;
+	ropt->dumpStatistics = dopt.dumpStatistics;
 	ropt->if_exists = dopt.if_exists;
 	ropt->column_inserts = dopt.column_inserts;
 	ropt->dumpSections = dopt.dumpSections;
@@ -1177,7 +1204,7 @@ help(const char *progname)
 	printf(_("  -?, --help                   show this help, then exit\n"));
 
 	printf(_("\nOptions controlling the output content:\n"));
-	printf(_("  -a, --data-only              dump only the data, not the schema\n"));
+	printf(_("  -a, --data-only              dump only the data, not the schema or statistics\n"));
 	printf(_("  -b, --large-objects          include large objects in dump\n"));
 	printf(_("  --blobs                      (same as --large-objects, deprecated)\n"));
 	printf(_("  -B, --no-large-objects       exclude large objects in dump\n"));
@@ -1190,11 +1217,12 @@ help(const char *progname)
 	printf(_("  -N, --exclude-schema=PATTERN do NOT dump the specified schema(s)\n"));
 	printf(_("  -O, --no-owner               skip restoration of object ownership in\n"
 			 "                               plain-text format\n"));
-	printf(_("  -s, --schema-only            dump only the schema, no data\n"));
+	printf(_("  -s, --schema-only            dump only the schema, no data or statistics\n"));
 	printf(_("  -S, --superuser=NAME         superuser user name to use in plain-text format\n"));
 	printf(_("  -t, --table=PATTERN          dump only the specified table(s)\n"));
 	printf(_("  -T, --exclude-table=PATTERN  do NOT dump the specified table(s)\n"));
 	printf(_("  -x, --no-privileges          do not dump privileges (grant/revoke)\n"));
+	printf(_("  -X, --statistics-only        dump only the statistics, not schema or data\n"));
 	printf(_("  --binary-upgrade             for use by upgrade utilities only\n"));
 	printf(_("  --column-inserts             dump data as INSERT commands with column names\n"));
 	printf(_("  --disable-dollar-quoting     disable dollar quoting, use SQL standard quoting\n"));
@@ -1221,6 +1249,7 @@ help(const char *progname)
 	printf(_("  --no-comments                do not dump comment commands\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
 	printf(_("  --no-tablespaces             do not dump tablespace assignments\n"));
@@ -6777,6 +6806,42 @@ getFuncs(Archive *fout)
 	destroyPQExpBuffer(query);
 }
 
+/*
+ * getRelationStatistics
+ *    register the statistics object as a dependent of the relation.
+ *
+ */
+static RelStatsInfo *
+getRelationStatistics(Archive *fout, DumpableObject *rel, char relkind)
+{
+	if ((relkind == RELKIND_RELATION) ||
+		(relkind == RELKIND_PARTITIONED_TABLE) ||
+		(relkind == RELKIND_INDEX) ||
+		(relkind == RELKIND_PARTITIONED_INDEX) ||
+		(relkind == RELKIND_MATVIEW))
+	{
+		RelStatsInfo	   *info = pg_malloc0(sizeof(RelStatsInfo));
+		DumpableObject	   *dobj = &info->dobj;
+
+		dobj->objType = DO_REL_STATS;
+		dobj->catId.tableoid = 0;
+		dobj->catId.oid = 0;
+		AssignDumpId(dobj);
+		dobj->dependencies = (DumpId *) pg_malloc(sizeof(DumpId));
+		dobj->dependencies[0] = rel->dumpId;
+		dobj->nDeps = 1;
+		dobj->allocDeps = 1;
+		dobj->components |= DUMP_COMPONENT_STATISTICS;
+		dobj->dump = rel->dump;
+		dobj->name = pg_strdup(rel->name);
+		dobj->namespace = rel->namespace;
+		info->relkind = relkind;
+
+		return info;
+	}
+	return NULL;
+}
+
 /*
  * getTables
  *	  read all the tables (no indexes) in the system catalogs,
@@ -7154,6 +7219,7 @@ getTables(Archive *fout, int *numTables)
 
 		/* Tables have data */
 		tblinfo[i].dobj.components |= DUMP_COMPONENT_DATA;
+		tblinfo[i].dobj.components |= DUMP_COMPONENT_STATISTICS;
 
 		/* Mark whether table has an ACL */
 		if (!PQgetisnull(res, i, i_relacl))
@@ -7202,6 +7268,8 @@ getTables(Archive *fout, int *numTables)
 				}
 			}
 		}
+		if (tblinfo[i].interesting)
+			getRelationStatistics(fout, &tblinfo[i].dobj, tblinfo[i].relkind);
 	}
 
 	if (query->len != 0)
@@ -7646,11 +7714,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 		for (int c = 0; c < numinds; c++, j++)
 		{
 			char		contype;
+			char		indexkind;
+			RelStatsInfo   *relstats;
 
 			indxinfo[j].dobj.objType = DO_INDEX;
 			indxinfo[j].dobj.catId.tableoid = atooid(PQgetvalue(res, j, i_tableoid));
 			indxinfo[j].dobj.catId.oid = atooid(PQgetvalue(res, j, i_oid));
 			AssignDumpId(&indxinfo[j].dobj);
+			indxinfo[j].dobj.components |= DUMP_COMPONENT_STATISTICS;
 			indxinfo[j].dobj.dump = tbinfo->dobj.dump;
 			indxinfo[j].dobj.name = pg_strdup(PQgetvalue(res, j, i_indexname));
 			indxinfo[j].dobj.namespace = tbinfo->dobj.namespace;
@@ -7673,7 +7744,14 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 			{
 				NULL, NULL
 			};
+
+			if (indxinfo[j].parentidx == 0)
+				indexkind = RELKIND_INDEX;
+			else
+				indexkind = RELKIND_PARTITIONED_INDEX;
+
 			contype = *(PQgetvalue(res, j, i_contype));
+			relstats = getRelationStatistics(fout, &indxinfo[j].dobj, indexkind);
 
 			if (contype == 'p' || contype == 'u' || contype == 'x')
 			{
@@ -7707,6 +7785,8 @@ getIndexes(Archive *fout, TableInfo tblinfo[], int numTables)
 				constrinfo->separate = true;
 
 				indxinfo[j].indexconstraint = constrinfo->dobj.dumpId;
+				if (relstats != NULL)
+					addObjectDependency(&relstats->dobj, constrinfo->dobj.dumpId);
 			}
 			else
 			{
@@ -10294,6 +10374,276 @@ dumpComment(Archive *fout, const char *type,
 						catalogId, subid, dumpId, NULL);
 }
 
+/*
+ * Tabular description of the parameters to pg_restore_relation_stats()
+ * param_name, param_type
+ */
+static const char *rel_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"version", "integer"},
+	{"relpages", "integer"},
+	{"reltuples", "real"},
+	{"relallvisible", "integer"},
+};
+
+/*
+ * Tabular description of the parameters to pg_restore_attribute_stats()
+ * param_name, param_type
+ */
+static const char *att_stats_arginfo[][2] = {
+	{"relation", "regclass"},
+	{"attname", "name"},
+	{"inherited", "boolean"},
+	{"version", "integer"},
+	{"null_frac", "float4"},
+	{"avg_width", "integer"},
+	{"n_distinct", "float4"},
+	{"most_common_vals", "text"},
+	{"most_common_freqs", "float4[]"},
+	{"histogram_bounds", "text"},
+	{"correlation", "float4"},
+	{"most_common_elems", "text"},
+	{"most_common_elem_freqs", "float4[]"},
+	{"elem_count_histogram", "float4[]"},
+	{"range_length_histogram", "text"},
+	{"range_empty_frac", "float4"},
+	{"range_bounds_histogram", "text"},
+};
+
+/*
+ * getRelStatsExportQuery --
+ *
+ * Generate a query that will fetch all relation (e.g. pg_class)
+ * stats for a given relation.
+ */
+static void
+getRelStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "current_setting('server_version_num') AS version, "
+						 "c.relpages, c.reltuples, c.relallvisible "
+						 "FROM pg_class c "
+						 "JOIN pg_namespace n "
+						 "ON n.oid = c.relnamespace "
+						 "WHERE n.nspname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND c.relname = ");
+	appendStringLiteralAH(query, relname, fout);
+}
+
+/*
+ * getAttStatsExportQuery --
+ *
+ * Generate a query that will fetch all attribute (e.g. pg_statistic)
+ * stats for a given relation.
+ */
+static void
+getAttStatsExportQuery(PQExpBuffer query, Archive *fout,
+					   const char *schemaname, const char *relname)
+{
+	resetPQExpBuffer(query);
+	appendPQExpBufferStr(query,
+						 "SELECT c.oid::regclass AS relation, "
+						 "s.attname,"
+						 "s.inherited,"
+						 "current_setting('server_version_num') AS version, "
+						 "s.null_frac,"
+						 "s.avg_width,"
+						 "s.n_distinct,"
+						 "s.most_common_vals,"
+						 "s.most_common_freqs,"
+						 "s.histogram_bounds,"
+						 "s.correlation,"
+						 "s.most_common_elems,"
+						 "s.most_common_elem_freqs,"
+						 "s.elem_count_histogram,");
+
+	if (fout->remoteVersion >= 170000)
+		appendPQExpBufferStr(query,
+							 "s.range_length_histogram,"
+							 "s.range_empty_frac,"
+							 "s.range_bounds_histogram ");
+	else
+		appendPQExpBufferStr(query,
+							 "NULL AS range_length_histogram,"
+							 "NULL AS range_empty_frac,"
+							 "NULL AS range_bounds_histogram ");
+
+	appendPQExpBufferStr(query,
+						 "FROM pg_stats s "
+						 "JOIN pg_namespace n "
+						 "ON n.nspname = s.schemaname "
+						 "JOIN pg_class c "
+						 "ON c.relname = s.tablename "
+						 "AND c.relnamespace = n.oid "
+						 "WHERE s.schemaname = ");
+	appendStringLiteralAH(query, schemaname, fout);
+	appendPQExpBufferStr(query, " AND s.tablename = ");
+	appendStringLiteralAH(query, relname, fout);
+	appendPQExpBufferStr(query, " ORDER BY s.attname, s.inherited");
+}
+
+
+/*
+ * appendNamedArgument --
+ *
+ * Convenience routine for constructing parameters of the form:
+ * 'paraname', 'value'::type
+ */
+static void
+appendNamedArgument(PQExpBuffer out, Archive *fout, const char *argname,
+					const char *argval, const char *argtype)
+{
+	appendPQExpBufferStr(out, "\t");
+
+	appendStringLiteralAH(out, argname, fout);
+	appendPQExpBufferStr(out, ", ");
+
+	appendStringLiteralAH(out, argval, fout);
+	appendPQExpBuffer(out, "::%s", argtype);
+}
+
+/*
+ * appendRelStatsImport --
+ *
+ * Append a formatted pg_restore_relation_stats statement.
+ */
+static void
+appendRelStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	const char *sep = "";
+
+	if (PQntuples(res) == 0)
+		return;
+
+	appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_relation_stats(\n");
+
+	for (int argno = 0; argno < lengthof(rel_stats_arginfo); argno++)
+	{
+		const char *argname = rel_stats_arginfo[argno][0];
+		const char *argtype = rel_stats_arginfo[argno][1];
+		int			fieldno = PQfnumber(res, argname);
+
+		if (fieldno < 0)
+			pg_fatal("relation stats export query missing field '%s'",
+					 argname);
+
+		if (PQgetisnull(res, 0, fieldno))
+			continue;
+
+		appendPQExpBufferStr(out, sep);
+		appendNamedArgument(out, fout, argname, PQgetvalue(res, 0, fieldno), argtype);
+
+		sep = ",\n";
+	}
+	appendPQExpBufferStr(out, "\n);\n");
+}
+
+/*
+ * appendAttStatsImport --
+ *
+ * Append a series of formatted pg_restore_attribute_stats statements.
+ */
+static void
+appendAttStatsImport(PQExpBuffer out, Archive *fout, PGresult *res)
+{
+	for (int rownum = 0; rownum < PQntuples(res); rownum++)
+	{
+		const char *sep = "";
+
+		appendPQExpBufferStr(out, "SELECT * FROM pg_catalog.pg_restore_attribute_stats(\n");
+		for (int argno = 0; argno < lengthof(att_stats_arginfo); argno++)
+		{
+			const char *argname = att_stats_arginfo[argno][0];
+			const char *argtype = att_stats_arginfo[argno][1];
+			int			fieldno = PQfnumber(res, argname);
+
+			if (fieldno < 0)
+				pg_fatal("attribute stats export query missing field '%s'",
+						 argname);
+
+			if (PQgetisnull(res, rownum, fieldno))
+				continue;
+
+			appendPQExpBufferStr(out, sep);
+			appendNamedArgument(out, fout, argname, PQgetvalue(res, rownum, fieldno), argtype);
+			sep = ",\n";
+		}
+		appendPQExpBufferStr(out, "\n);\n");
+	}
+}
+
+/*
+ * get statistics dump section, which depends on the parent object type
+ *
+ * objects created in SECTION_PRE_DATA have stats in SECTION_DATA
+ * objects created in SECTION_POST_DATA have stats in SECTION_POST_DATA
+ */
+static teSection
+statisticsDumpSection(const RelStatsInfo *rsinfo)
+{
+
+	if ((rsinfo->relkind == RELKIND_MATVIEW) ||
+		(rsinfo->relkind == RELKIND_INDEX) ||
+		(rsinfo->relkind == RELKIND_PARTITIONED_INDEX))
+			return SECTION_POST_DATA;
+
+	return SECTION_DATA;
+}
+
+/*
+ * dumpRelationStats --
+ *
+ * Dump command to import stats into the relation on the new database.
+ */
+static void
+dumpRelationStats(Archive *fout, const RelStatsInfo *rsinfo)
+{
+	PGresult	   *res;
+	PQExpBuffer		query;
+	PQExpBuffer		out;
+	PQExpBuffer		tag;
+	DumpableObject *dobj = (DumpableObject *) &rsinfo->dobj;
+
+	/* nothing to do if we are not dumping statistics */
+	if (!fout->dopt->dumpStatistics)
+		return;
+
+	tag = createPQExpBuffer();
+	appendPQExpBuffer(tag, "%s %s", "STATISTICS DATA", fmtId(dobj->name));
+
+	query = createPQExpBuffer();
+	out = createPQExpBuffer();
+
+	getRelStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendRelStatsImport(out, fout, res);
+	PQclear(res);
+
+	getAttStatsExportQuery(query, fout, dobj->namespace->dobj.name,
+						   dobj->name);
+	res = ExecuteSqlQuery(fout, query->data, PGRES_TUPLES_OK);
+	appendAttStatsImport(out, fout, res);
+	PQclear(res);
+
+	ArchiveEntry(fout, nilCatalogId, createDumpId(),
+				 ARCHIVE_OPTS(.tag = tag->data,
+							  .namespace = dobj->namespace->dobj.name,
+							  .description = "STATISTICS DATA",
+							  .section = statisticsDumpSection(rsinfo),
+							  .createStmt = out->data,
+							  .deps = dobj->dependencies,
+							  .nDeps = dobj->nDeps));
+
+	destroyPQExpBuffer(query);
+	destroyPQExpBuffer(out);
+	destroyPQExpBuffer(tag);
+}
+
 /*
  * dumpTableComment --
  *
@@ -10742,6 +11092,9 @@ dumpDumpableObject(Archive *fout, DumpableObject *dobj)
 		case DO_SUBSCRIPTION_REL:
 			dumpSubscriptionTable(fout, (const SubRelInfo *) dobj);
 			break;
+		case DO_REL_STATS:
+			dumpRelationStats(fout, (const RelStatsInfo *) dobj);
+			break;
 		case DO_PRE_DATA_BOUNDARY:
 		case DO_POST_DATA_BOUNDARY:
 			/* never dumped, nothing to do */
@@ -17172,6 +17525,8 @@ dumpIndex(Archive *fout, const IndxInfo *indxinfo)
 		free(indstatvalsarray);
 	}
 
+	/* Comments and stats share same .dep */
+
 	/* Dump Index Comments */
 	if (indxinfo->dobj.dump & DUMP_COMPONENT_COMMENT)
 		dumpComment(fout, "INDEX", qindxname,
@@ -18959,6 +19314,16 @@ addBoundaryDependencies(DumpableObject **dobjs, int numObjs,
 				/* must come after the pre-data boundary */
 				addObjectDependency(dobj, preDataBound->dumpId);
 				break;
+			case DO_REL_STATS:
+				/* stats section varies by parent object type, DATA or POST */
+				if (statisticsDumpSection((RelStatsInfo *) dobj) == SECTION_DATA)
+				{
+					addObjectDependency(dobj, preDataBound->dumpId);
+					addObjectDependency(postDataBound, dobj->dumpId);
+				}
+				else
+					addObjectDependency(dobj, postDataBound->dumpId);
+				break;
 		}
 	}
 }
diff --git a/src/bin/pg_dump/pg_dump.h b/src/bin/pg_dump/pg_dump.h
index 2e55a0e3bb..def5f292e6 100644
--- a/src/bin/pg_dump/pg_dump.h
+++ b/src/bin/pg_dump/pg_dump.h
@@ -84,6 +84,7 @@ typedef enum
 	DO_PUBLICATION_TABLE_IN_SCHEMA,
 	DO_SUBSCRIPTION,
 	DO_SUBSCRIPTION_REL,		/* see note for SubRelInfo */
+	DO_REL_STATS,
 } DumpableObjectType;
 
 /*
@@ -101,6 +102,7 @@ typedef uint32 DumpComponents;
 #define DUMP_COMPONENT_ACL			(1 << 4)
 #define DUMP_COMPONENT_POLICY		(1 << 5)
 #define DUMP_COMPONENT_USERMAP		(1 << 6)
+#define DUMP_COMPONENT_STATISTICS	(1 << 7)
 #define DUMP_COMPONENT_ALL			(0xFFFF)
 
 /*
@@ -421,6 +423,12 @@ typedef struct _indexAttachInfo
 	IndxInfo   *partitionIdx;	/* link to index on partition */
 } IndexAttachInfo;
 
+typedef struct _relStatsInfo
+{
+	DumpableObject	dobj;
+	char			relkind;		/* 'r', 'v', 'c', etc */
+} RelStatsInfo;
+
 typedef struct _statsExtInfo
 {
 	DumpableObject dobj;
diff --git a/src/bin/pg_dump/pg_dump_sort.c b/src/bin/pg_dump/pg_dump_sort.c
index 3c8f2eb808..989d20aa27 100644
--- a/src/bin/pg_dump/pg_dump_sort.c
+++ b/src/bin/pg_dump/pg_dump_sort.c
@@ -1500,6 +1500,11 @@ describeDumpableObject(DumpableObject *obj, char *buf, int bufsize)
 					 "POST-DATA BOUNDARY  (ID %d)",
 					 obj->dumpId);
 			return;
+		case DO_REL_STATS:
+			snprintf(buf, bufsize,
+					 "RELATION STATISTICS FOR %s  (ID %d OID %u)",
+					 obj->name, obj->dumpId, obj->catId.oid);
+			return;
 	}
 	/* shouldn't get here */
 	snprintf(buf, bufsize,
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 9a04e51c81..62e2766c09 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -103,6 +103,7 @@ static int	use_setsessauth = 0;
 static int	no_comments = 0;
 static int	no_publications = 0;
 static int	no_security_labels = 0;
+static int	no_statistics = 0;
 static int	no_subscriptions = 0;
 static int	no_toast_compression = 0;
 static int	no_unlogged_table_data = 0;
@@ -172,6 +173,7 @@ main(int argc, char *argv[])
 		{"no-role-passwords", no_argument, &no_role_passwords, 1},
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
+		{"no-statistics", no_argument, &no_statistics, 1},
 		{"no-sync", no_argument, NULL, 4},
 		{"no-toast-compression", no_argument, &no_toast_compression, 1},
 		{"no-unlogged-table-data", no_argument, &no_unlogged_table_data, 1},
@@ -451,6 +453,8 @@ main(int argc, char *argv[])
 		appendPQExpBufferStr(pgdumpopts, " --no-publications");
 	if (no_security_labels)
 		appendPQExpBufferStr(pgdumpopts, " --no-security-labels");
+	if (no_statistics)
+		appendPQExpBufferStr(pgdumpopts, " --no-statistics");
 	if (no_subscriptions)
 		appendPQExpBufferStr(pgdumpopts, " --no-subscriptions");
 	if (no_toast_compression)
@@ -666,6 +670,7 @@ help(void)
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-role-passwords          do not dump passwords for roles\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
+	printf(_("  --no-statistics              do not dump statistics\n"));
 	printf(_("  --no-subscriptions           do not dump subscriptions\n"));
 	printf(_("  --no-sync                    do not wait for changes to be written safely to disk\n"));
 	printf(_("  --no-table-access-method     do not dump table access methods\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 88ae39d938..355f0439da 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -63,6 +63,7 @@ main(int argc, char **argv)
 	int			numWorkers = 1;
 	Archive    *AH;
 	char	   *inputFileSpec;
+	bool		statistics_only = false;
 	static int	disable_triggers = 0;
 	static int	enable_row_security = 0;
 	static int	if_exists = 0;
@@ -74,6 +75,7 @@ main(int argc, char **argv)
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
 	static int	no_subscriptions = 0;
+	static int  no_statistics = 0;
 	static int	strict_names = 0;
 	bool		data_only = false;
 	bool		schema_only = false;
@@ -108,6 +110,7 @@ main(int argc, char **argv)
 		{"username", 1, NULL, 'U'},
 		{"verbose", 0, NULL, 'v'},
 		{"single-transaction", 0, NULL, '1'},
+		{"statistics-only", no_argument, NULL, 'P'},
 
 		/*
 		 * the following options don't have an equivalent short option letter
@@ -128,6 +131,7 @@ main(int argc, char **argv)
 		{"no-security-labels", no_argument, &no_security_labels, 1},
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
+		{"no-statistics", no_argument, &no_statistics, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -271,6 +275,10 @@ main(int argc, char **argv)
 				opts->aclsSkip = 1;
 				break;
 
+			case 'X':			/* Restore statistics only */
+				statistics_only = true;
+				break;
+
 			case '1':			/* Restore data in a single transaction */
 				opts->single_txn = true;
 				opts->exit_on_error = true;
@@ -343,6 +351,10 @@ main(int argc, char **argv)
 
 	if (data_only && schema_only)
 		pg_fatal("options -s/--schema-only and -a/--data-only cannot be used together");
+	if (data_only && statistics_only)
+		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
+	if (schema_only && statistics_only)
+		pg_fatal("options -s/--schema-only and -X/--statistics-only cannot be used together");
 
 	if (data_only && opts->dropSchema)
 		pg_fatal("options -c/--clean and -a/--data-only cannot be used together");
@@ -362,8 +374,9 @@ main(int argc, char **argv)
 		pg_fatal("cannot specify both --single-transaction and multiple jobs");
 
 	/* set derivative flags */
-	opts->dumpSchema = (!data_only);
-	opts->dumpData = (!schema_only);
+	opts->dumpSchema = (!data_only && !statistics_only);
+	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
 	opts->enable_row_security = enable_row_security;
@@ -375,6 +388,7 @@ main(int argc, char **argv)
 	opts->no_publications = no_publications;
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
+	opts->no_statistics = no_statistics;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
diff --git a/src/bin/pg_dump/t/001_basic.pl b/src/bin/pg_dump/t/001_basic.pl
index b9d13a0e1d..76ebf15f55 100644
--- a/src/bin/pg_dump/t/001_basic.pl
+++ b/src/bin/pg_dump/t/001_basic.pl
@@ -50,12 +50,30 @@ command_fails_like(
 	'pg_dump: options -s/--schema-only and -a/--data-only cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '-s', '-X' ],
+	qr/\Qpg_dump: error: options -s\/--schema-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -s/--schema-only and -X/--statistics-only cannot be used together'
+);
+
+command_fails_like(
+	[ 'pg_dump', '-a', '-X' ],
+	qr/\Qpg_dump: error: options -a\/--data-only and -X\/--statistics-only cannot be used together\E/,
+	'pg_dump: error: options -a/--data-only and -X/--statistics-only cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-s', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: options -s\/--schema-only and --include-foreign-data cannot be used together\E/,
 	'pg_dump: options -s/--schema-only and --include-foreign-data cannot be used together'
 );
 
+command_fails_like(
+	[ 'pg_dump', '--statistics-only', '--no-statistics' ],
+	qr/\Qpg_dump: error: options -X\/--statistics-only and --no-statistics cannot be used together\E/,
+	'pg_dump: options -X\/--statistics-only and --no-statistics cannot be used together'
+);
+
 command_fails_like(
 	[ 'pg_dump', '-j2', '--include-foreign-data=xxx' ],
 	qr/\Qpg_dump: error: option --include-foreign-data is not supported with parallel backup\E/,
diff --git a/doc/src/sgml/ref/pg_dump.sgml b/doc/src/sgml/ref/pg_dump.sgml
index d66e901f51..aa4785c612 100644
--- a/doc/src/sgml/ref/pg_dump.sgml
+++ b/doc/src/sgml/ref/pg_dump.sgml
@@ -123,7 +123,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Dump only the data, not the schema (data definitions).
+        Dump only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are dumped.
        </para>
 
@@ -141,8 +141,9 @@ PostgreSQL documentation
       <listitem>
        <para>
         Include large objects in the dump.  This is the default behavior
-        except when <option>--schema</option>, <option>--table</option>, or
-        <option>--schema-only</option> is specified.  The <option>-b</option>
+        except when <option>--schema</option>, <option>--table</option>,
+        <option>--schema-only</option>, or
+        <option>--statistics-only</option> is specified.  The <option>-b</option>
         switch is therefore only useful to add large objects to dumps
         where a specific schema or table has been requested.  Note that
         large objects are considered data and therefore will be included when
@@ -516,10 +517,11 @@ PostgreSQL documentation
       <term><option>--schema-only</option></term>
       <listitem>
        <para>
-        Dump only the object definitions (schema), not data.
+        Dump only the object definitions (schema), not data or statistics.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive to <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
         <option>--section=pre-data --section=post-data</option>.
@@ -652,6 +654,18 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Dump only the statistics, not the schema (data definitions) or data.
+        Statistics for tables, materialized views, and indexes are dumped.
+       </para>
+
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-Z <replaceable class="parameter">level</replaceable></option></term>
       <term><option>-Z <replaceable class="parameter">method</replaceable></option>[:<replaceable>detail</replaceable>]</term>
@@ -833,7 +847,8 @@ PostgreSQL documentation
         though you do not need the data in it.
        </para>
        <para>
-        To exclude data for all tables in the database, see <option>--schema-only</option>.
+        To exclude data for all tables in the database, see <option>--schema-only</option>
+        or <option>--statistics-only</option>.
        </para>
       </listitem>
      </varlistentry>
@@ -1098,6 +1113,15 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not dump statistics.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
diff --git a/doc/src/sgml/ref/pg_restore.sgml b/doc/src/sgml/ref/pg_restore.sgml
index b8b27e1719..ff1441a243 100644
--- a/doc/src/sgml/ref/pg_restore.sgml
+++ b/doc/src/sgml/ref/pg_restore.sgml
@@ -94,7 +94,7 @@ PostgreSQL documentation
       <term><option>--data-only</option></term>
       <listitem>
        <para>
-        Restore only the data, not the schema (data definitions).
+        Restore only the data, not the schema (data definitions) or statistics.
         Table data, large objects, and sequence values are restored,
         if present in the archive.
        </para>
@@ -483,10 +483,11 @@ PostgreSQL documentation
         to the extent that schema entries are present in the archive.
        </para>
        <para>
-        This option is the inverse of <option>--data-only</option>.
+        This option is mutually exclusive of <option>--data-only</option>
+        and <option>--statistics-only</option>.
         It is similar to, but for historical reasons not identical to,
         specifying
-        <option>--section=pre-data --section=post-data</option>.
+        <option>--section=pre-data --section=post-data --no-statistics</option>.
        </para>
        <para>
         (Do not confuse this with the <option>--schema</option> option, which
@@ -599,6 +600,20 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>-X</option></term>
+      <term><option>--statistics-only</option></term>
+      <listitem>
+       <para>
+        Restore only the statistics, not schema or data.
+       </para>
+       <para>
+        (Do not confuse this with the <option>--schema</option> option, which
+        uses the word <quote>schema</quote> in a different meaning.)
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>-1</option></term>
       <term><option>--single-transaction</option></term>
@@ -723,6 +738,16 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+     <varlistentry>
+      <term><option>--no-statistics</option></term>
+      <listitem>
+       <para>
+        Do not output commands to restore statistics, even if the archive
+        contains them.
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry>
       <term><option>--no-subscriptions</option></term>
       <listitem>
-- 
2.47.1

From ab4a753d50cd2fbea7e8cd3e8b1dfa37f4ac9508 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Wed, 11 Dec 2024 22:38:00 -0500
Subject: [PATCH v35 05/12] Add statistics flags to pg_upgrade

---
 src/bin/pg_upgrade/dump.c       |  6 ++++--
 src/bin/pg_upgrade/option.c     | 12 ++++++++++++
 src/bin/pg_upgrade/pg_upgrade.h |  1 +
 3 files changed, 17 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index ef2f14a79b..954e7bacf4 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -21,10 +21,11 @@ generate_old_dump(void)
 
 	/* run new pg_dumpall binary for globals */
 	exec_prog(UTILITY_LOG_FILE, NULL, true, true,
-			  "\"%s/pg_dumpall\" %s --globals-only --quote-all-identifiers "
+			  "\"%s/pg_dumpall\" %s %s --globals-only --quote-all-identifiers "
 			  "--binary-upgrade %s --no-sync -f \"%s/%s\"",
 			  new_cluster.bindir, cluster_conn_opts(&old_cluster),
 			  log_opts.verbose ? "--verbose" : "",
+			  user_opts.do_statistics ? "" : "--no-statistics",
 			  log_opts.dumpdir,
 			  GLOBALS_DUMP_FILE);
 	check_ok();
@@ -52,10 +53,11 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --no-data --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data %s --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
+						   user_opts.do_statistics ? "" : "--no-statistics",
 						   log_opts.dumpdir,
 						   sql_file_name, escaped_connstr.data);
 
diff --git a/src/bin/pg_upgrade/option.c b/src/bin/pg_upgrade/option.c
index 6f41d63eed..76a0bb2991 100644
--- a/src/bin/pg_upgrade/option.c
+++ b/src/bin/pg_upgrade/option.c
@@ -60,6 +60,8 @@ parseCommandLine(int argc, char *argv[])
 		{"copy", no_argument, NULL, 2},
 		{"copy-file-range", no_argument, NULL, 3},
 		{"sync-method", required_argument, NULL, 4},
+		{"with-statistics", no_argument, NULL, 5},
+		{"no-statistics", no_argument, NULL, 6},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -70,6 +72,7 @@ parseCommandLine(int argc, char *argv[])
 
 	user_opts.do_sync = true;
 	user_opts.transfer_mode = TRANSFER_MODE_COPY;
+	user_opts.do_statistics = true;
 
 	os_info.progname = get_progname(argv[0]);
 
@@ -212,6 +215,13 @@ parseCommandLine(int argc, char *argv[])
 				user_opts.sync_method = pg_strdup(optarg);
 				break;
 
+			case 5:
+				user_opts.do_statistics = true;
+				break;
+			case 6:
+				user_opts.do_statistics = false;
+				break;
+
 			default:
 				fprintf(stderr, _("Try \"%s --help\" for more information.\n"),
 						os_info.progname);
@@ -307,6 +317,8 @@ usage(void)
 	printf(_("  --copy                        copy files to new cluster (default)\n"));
 	printf(_("  --copy-file-range             copy files to new cluster with copy_file_range\n"));
 	printf(_("  --sync-method=METHOD          set method for syncing files to disk\n"));
+	printf(_("  --with-statisttics            import statistics from old cluster (default)\n"));
+	printf(_("  --no-statisttics              do not import statistics from old cluster\n"));
 	printf(_("  -?, --help                    show this help, then exit\n"));
 	printf(_("\n"
 			 "Before running pg_upgrade you must:\n"
diff --git a/src/bin/pg_upgrade/pg_upgrade.h b/src/bin/pg_upgrade/pg_upgrade.h
index 53f693c2d4..371f9a2d06 100644
--- a/src/bin/pg_upgrade/pg_upgrade.h
+++ b/src/bin/pg_upgrade/pg_upgrade.h
@@ -327,6 +327,7 @@ typedef struct
 	int			jobs;			/* number of processes/threads to use */
 	char	   *socketdir;		/* directory to use for Unix sockets */
 	char	   *sync_method;
+	bool		do_statistics;	/* carry over statistics from old cluster */
 } UserOpts;
 
 typedef struct
-- 
2.47.1

From 436f30b7bce2cd6eb9904384ca0395a7abab6bfd Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Thu, 14 Nov 2024 04:58:17 -0500
Subject: [PATCH v35 02/12] Add --no-data option.

This option is useful for situations where someone wishes to test
query plans from a production database without copying production data.
---
 src/bin/pg_dump/pg_backup.h          | 2 ++
 src/bin/pg_dump/pg_backup_archiver.c | 2 ++
 src/bin/pg_dump/pg_dump.c            | 7 ++++++-
 src/bin/pg_dump/pg_restore.c         | 5 ++++-
 4 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/bin/pg_dump/pg_backup.h b/src/bin/pg_dump/pg_backup.h
index 8fbb39d399..241855d017 100644
--- a/src/bin/pg_dump/pg_backup.h
+++ b/src/bin/pg_dump/pg_backup.h
@@ -110,6 +110,7 @@ typedef struct _restoreOptions
 	int			column_inserts;
 	int			if_exists;
 	int			no_comments;	/* Skip comments */
+	int			no_data;			/* Skip data */
 	int			no_publications;	/* Skip publication entries */
 	int			no_security_labels; /* Skip security label entries */
 	int			no_subscriptions;	/* Skip subscription entries */
@@ -185,6 +186,7 @@ typedef struct _dumpOptions
 	int			no_publications;
 	int			no_subscriptions;
 	int			no_statistics;
+	int			no_data;
 	int			no_toast_compression;
 	int			no_unlogged_table_data;
 	int			serializable_deferrable;
diff --git a/src/bin/pg_dump/pg_backup_archiver.c b/src/bin/pg_dump/pg_backup_archiver.c
index 185d7fbb7e..41001e64ac 100644
--- a/src/bin/pg_dump/pg_backup_archiver.c
+++ b/src/bin/pg_dump/pg_backup_archiver.c
@@ -186,6 +186,8 @@ dumpOptionsFromRestoreOptions(RestoreOptions *ropt)
 	dopt->no_publications = ropt->no_publications;
 	dopt->no_security_labels = ropt->no_security_labels;
 	dopt->no_subscriptions = ropt->no_subscriptions;
+	dopt->no_data = ropt->no_data;
+	dopt->no_statistics = ropt->no_statistics;
 	dopt->lockWaitTimeout = ropt->lockWaitTimeout;
 	dopt->include_everything = ropt->include_everything;
 	dopt->enable_row_security = ropt->enable_row_security;
diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index 8f85b04c33..c8a0b4afdf 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -494,6 +494,7 @@ main(int argc, char **argv)
 		{"strict-names", no_argument, &strict_names, 1},
 		{"use-set-session-authorization", no_argument, &dopt.use_setsessauth, 1},
 		{"no-comments", no_argument, &dopt.no_comments, 1},
+		{"no-data", no_argument, &dopt.no_data, 1},
 		{"no-publications", no_argument, &dopt.no_publications, 1},
 		{"no-security-labels", no_argument, &dopt.no_security_labels, 1},
 		{"no-statistics", no_argument, &dopt.no_statistics, 1},
@@ -796,6 +797,9 @@ main(int argc, char **argv)
 	if (data_only && statistics_only)
 		pg_fatal("options -a/--data-only and -X/--statistics-only cannot be used together");
 
+	if (data_only && dopt.no_data)
+		pg_fatal("options -a/--data-only and --no-data cannot be used together");
+
 	if (statistics_only && dopt.no_statistics)
 		pg_fatal("options -X/--statistics-only and --no-statistics cannot be used together");
 
@@ -812,7 +816,7 @@ main(int argc, char **argv)
 		pg_fatal("option --if-exists requires option -c/--clean");
 
 	/* set derivative flags */
-	dopt.dumpData = data_only || (!schema_only && !statistics_only);
+	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
 	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
 
 	if (statistics_only)
@@ -1247,6 +1251,7 @@ help(const char *progname)
 	printf(_("  --inserts                    dump data as INSERT commands, rather than COPY\n"));
 	printf(_("  --load-via-partition-root    load partitions via the root table\n"));
 	printf(_("  --no-comments                do not dump comment commands\n"));
+	printf(_("  --no-data                    do not dump data\n"));
 	printf(_("  --no-publications            do not dump publications\n"));
 	printf(_("  --no-security-labels         do not dump security label assignments\n"));
 	printf(_("  --no-statistics              do not dump statistics\n"));
diff --git a/src/bin/pg_dump/pg_restore.c b/src/bin/pg_dump/pg_restore.c
index 355f0439da..31c3cd32de 100644
--- a/src/bin/pg_dump/pg_restore.c
+++ b/src/bin/pg_dump/pg_restore.c
@@ -71,6 +71,7 @@ main(int argc, char **argv)
 	static int	outputNoTableAm = 0;
 	static int	outputNoTablespaces = 0;
 	static int	use_setsessauth = 0;
+	static int	no_data = 0;
 	static int	no_comments = 0;
 	static int	no_publications = 0;
 	static int	no_security_labels = 0;
@@ -132,6 +133,7 @@ main(int argc, char **argv)
 		{"no-subscriptions", no_argument, &no_subscriptions, 1},
 		{"filter", required_argument, NULL, 4},
 		{"no-statistics", no_argument, &no_statistics, 1},
+		{"no-data", no_argument, &no_data, 1},
 
 		{NULL, 0, NULL, 0}
 	};
@@ -375,7 +377,7 @@ main(int argc, char **argv)
 
 	/* set derivative flags */
 	opts->dumpSchema = (!data_only && !statistics_only);
-	opts->dumpData = (!schema_only && !statistics_only);
+	opts->dumpData = (!no_data && !schema_only && !statistics_only);
 	opts->dumpStatistics = (!no_statistics && !data_only && !schema_only);
 
 	opts->disable_triggers = disable_triggers;
@@ -389,6 +391,7 @@ main(int argc, char **argv)
 	opts->no_security_labels = no_security_labels;
 	opts->no_subscriptions = no_subscriptions;
 	opts->no_statistics = no_statistics;
+	opts->no_data = no_data;
 
 	if (if_exists && !opts->dropSchema)
 		pg_fatal("option --if-exists requires option -c/--clean");
-- 
2.47.1

From 6b18d68111bcf25e66d76e792a2ebdc4543094cb Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Wed, 11 Dec 2024 14:28:54 -0500
Subject: [PATCH v35 03/12] Change pg_upgrade's invocation of pg_dump to use
 --no-data

---
 src/bin/pg_dump/pg_dump.c | 13 +------------
 src/bin/pg_upgrade/dump.c |  2 +-
 2 files changed, 2 insertions(+), 13 deletions(-)

diff --git a/src/bin/pg_dump/pg_dump.c b/src/bin/pg_dump/pg_dump.c
index c8a0b4afdf..e626243f3a 100644
--- a/src/bin/pg_dump/pg_dump.c
+++ b/src/bin/pg_dump/pg_dump.c
@@ -818,18 +818,7 @@ main(int argc, char **argv)
 	/* set derivative flags */
 	dopt.dumpData = data_only || (!schema_only && !statistics_only && !dopt.no_data);
 	dopt.dumpSchema = schema_only || (!data_only && !statistics_only);
-
-	if (statistics_only)
-		/* stats are only thing wanted */
-		dopt.dumpStatistics = true;
-	else if (dopt.no_statistics)
-		/* stats specifically excluded */
-		dopt.dumpStatistics = false;
-	else if (dopt.binary_upgrade)
-		/* binary upgrade and not specifically excluded */
-		dopt.dumpStatistics = true;
-	else
-		dopt.dumpStatistics = (!data_only && !schema_only);
+	dopt.dumpStatistics = statistics_only || (!data_only && !schema_only && !dopt.no_statistics);
 
 	/*
 	 * --inserts are already implied above if --column-inserts or
diff --git a/src/bin/pg_upgrade/dump.c b/src/bin/pg_upgrade/dump.c
index 8345f55be8..ef2f14a79b 100644
--- a/src/bin/pg_upgrade/dump.c
+++ b/src/bin/pg_upgrade/dump.c
@@ -52,7 +52,7 @@ generate_old_dump(void)
 		snprintf(log_file_name, sizeof(log_file_name), DB_DUMP_LOG_FILE_MASK, old_db->db_oid);
 
 		parallel_exec_prog(log_file_name, NULL,
-						   "\"%s/pg_dump\" %s --schema-only --quote-all-identifiers "
+						   "\"%s/pg_dump\" %s --no-data --quote-all-identifiers "
 						   "--binary-upgrade --format=custom %s --no-sync --file=\"%s/%s\" %s",
 						   new_cluster.bindir, cluster_conn_opts(&old_cluster),
 						   log_opts.verbose ? "--verbose" : "",
-- 
2.47.1

From 4702b7b21f8b306d66650bb4e8354de9a131522c Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Wed, 6 Nov 2024 15:20:07 -0500
Subject: [PATCH v35 07/12] split out check_conn_options

---
 src/bin/scripts/vacuumdb.c | 103 ++++++++++++++++++++-----------------
 1 file changed, 57 insertions(+), 46 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index d07ab7d67e..7b97a9428a 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -10,6 +10,7 @@
  *-------------------------------------------------------------------------
  */
 
+#include "libpq-fe.h"
 #include "postgres_fe.h"
 
 #include <limits.h>
@@ -459,55 +460,11 @@ escape_quotes(const char *src)
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
+ * Check connection options for compatibility with the connected database.
  */
 static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
-	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
-	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
-	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
-
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
-
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
 	if (vacopts->disable_page_skipping && PQserverVersion(conn) < 90600)
 	{
 		PQfinish(conn);
@@ -575,6 +532,60 @@ vacuum_one_database(ConnParams *cparams,
 
 	/* skip_database_stats is used automatically if server supports it */
 	vacopts->skip_database_stats = (PQserverVersion(conn) >= 160000);
+}
+
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PQExpBufferData buf;
+	PQExpBufferData catalog_query;
+	PGresult   *res;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	SimpleStringList dbtables = {NULL, NULL};
+	int			i;
+	int			ntups;
+	bool		failed = false;
+	bool		objects_listed = false;
+	const char *initcmd;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
 
 	if (!quiet)
 	{
-- 
2.47.1

From 8dbae3174f3a6c8bb72beb577dc5c32ccb996e65 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Thu, 14 Nov 2024 03:53:33 -0500
Subject: [PATCH v35 06/12] Consolidate attribute syscache lookups into one
 call by name.

Previously we were doing one lookup by attname and one lookup by attnum,
which seems wasteful.
---
 src/backend/statistics/attribute_stats.c | 55 +++++++++++-------------
 1 file changed, 24 insertions(+), 31 deletions(-)

diff --git a/src/backend/statistics/attribute_stats.c b/src/backend/statistics/attribute_stats.c
index b97ba7b0c0..6393783f8e 100644
--- a/src/backend/statistics/attribute_stats.c
+++ b/src/backend/statistics/attribute_stats.c
@@ -78,8 +78,8 @@ static struct StatsArgInfo attarginfo[] =
 
 static bool attribute_statistics_update(FunctionCallInfo fcinfo, int elevel);
 static Node *get_attr_expr(Relation rel, int attnum);
-static void get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-							   Oid *atttypid, int32 *atttypmod,
+static void get_attr_stat_type(Oid reloid, Name attname, int elevel,
+							   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 							   char *atttyptype, Oid *atttypcoll,
 							   Oid *eq_opr, Oid *lt_opr);
 static bool get_elem_stat_type(Oid atttypid, char atttyptype, int elevel,
@@ -166,23 +166,16 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 
 	stats_check_required_arg(fcinfo, attarginfo, ATTNAME_ARG);
 	attname = PG_GETARG_NAME(ATTNAME_ARG);
-	attnum = get_attnum(reloid, NameStr(*attname));
-
-	if (attnum < 0)
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("cannot modify statistics on system column \"%s\"",
-						NameStr(*attname))));
-
-	if (attnum == InvalidAttrNumber)
-		ereport(ERROR,
-				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("column \"%s\" of relation \"%s\" does not exist",
-						NameStr(*attname), get_rel_name(reloid))));
 
 	stats_check_required_arg(fcinfo, attarginfo, INHERITED_ARG);
 	inherited = PG_GETARG_BOOL(INHERITED_ARG);
 
+	/* derive type information from attribute */
+	get_attr_stat_type(reloid, attname, elevel,
+					   &attnum, &atttypid, &atttypmod,
+					   &atttyptype, &atttypcoll,
+					   &eq_opr, &lt_opr);
+
 	/*
 	 * Check argument sanity. If some arguments are unusable, emit at elevel
 	 * and set the corresponding argument to NULL in fcinfo.
@@ -232,12 +225,6 @@ attribute_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		result = false;
 	}
 
-	/* derive information from attribute */
-	get_attr_stat_type(reloid, attnum, elevel,
-					   &atttypid, &atttypmod,
-					   &atttyptype, &atttypcoll,
-					   &eq_opr, &lt_opr);
-
 	/* if needed, derive element type */
 	if (do_mcelem || do_dechist)
 	{
@@ -503,8 +490,8 @@ get_attr_expr(Relation rel, int attnum)
  * Derive type information from the attribute.
  */
 static void
-get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
-				   Oid *atttypid, int32 *atttypmod,
+get_attr_stat_type(Oid reloid, Name attname, int elevel,
+				   AttrNumber *attnum, Oid *atttypid, int32 *atttypmod,
 				   char *atttyptype, Oid *atttypcoll,
 				   Oid *eq_opr, Oid *lt_opr)
 {
@@ -514,24 +501,30 @@ get_attr_stat_type(Oid reloid, AttrNumber attnum, int elevel,
 	Node	   *expr;
 	TypeCacheEntry *typcache;
 
-	atup = SearchSysCache2(ATTNUM, ObjectIdGetDatum(reloid),
-						   Int16GetDatum(attnum));
+	atup = SearchSysCacheAttName(reloid, NameStr(*attname));
 
-	/* Attribute not found */
+	/* Attribute not found or is dropped */
 	if (!HeapTupleIsValid(atup))
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
 	attr = (Form_pg_attribute) GETSTRUCT(atup);
 
-	if (attr->attisdropped)
+	if (attr->attnum < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("cannot modify statistics on system column \"%s\"",
+						NameStr(*attname))));
+
+	if (attr->attnum == InvalidAttrNumber)
 		ereport(ERROR,
 				(errcode(ERRCODE_UNDEFINED_COLUMN),
-				 errmsg("attribute %d of relation \"%s\" does not exist",
-						attnum, RelationGetRelationName(rel))));
+				 errmsg("column \"%s\" of relation \"%s\" does not exist",
+						NameStr(*attname), get_rel_name(reloid))));
 
+	*attnum = attr->attnum;
 	expr = get_attr_expr(rel, attr->attnum);
 
 	/*
-- 
2.47.1

From 1e405e14378c9184bad4bde4e19f2dda962df168 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Wed, 6 Nov 2024 15:46:51 -0500
Subject: [PATCH v35 08/12] split out print_processing_notice

---
 src/bin/scripts/vacuumdb.c | 41 +++++++++++++++++++++++---------------
 1 file changed, 25 insertions(+), 16 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 7b97a9428a..e9946f79b2 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -535,6 +535,30 @@ check_conn_options(PGconn *conn, vacuumingOptions *vacopts)
 }
 
 
+/*
+ * print the processing notice for a database.
+ */
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet)
+{
+	const char *stage_messages[] = {
+		gettext_noop("Generating minimal optimizer statistics (1 target)"),
+		gettext_noop("Generating medium optimizer statistics (10 targets)"),
+		gettext_noop("Generating default (full) optimizer statistics")
+	};
+
+	if (!quiet)
+	{
+		if (stage != ANALYZE_NO_STAGE)
+			printf(_("%s: processing database \"%s\": %s\n"),
+				   progname, PQdb(conn), _(stage_messages[stage]));
+		else
+			printf(_("%s: vacuuming database \"%s\"\n"),
+				   progname, PQdb(conn));
+		fflush(stdout);
+	}
+}
+
 /*
  * vacuum_one_database
  *
@@ -574,11 +598,6 @@ vacuum_one_database(ConnParams *cparams,
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
 		"RESET default_statistics_target;"
 	};
-	const char *stage_messages[] = {
-		gettext_noop("Generating minimal optimizer statistics (1 target)"),
-		gettext_noop("Generating medium optimizer statistics (10 targets)"),
-		gettext_noop("Generating default (full) optimizer statistics")
-	};
 
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
@@ -586,17 +605,7 @@ vacuum_one_database(ConnParams *cparams,
 	conn = connectDatabase(cparams, progname, echo, false, true);
 
 	check_conn_options(conn, vacopts);
-
-	if (!quiet)
-	{
-		if (stage != ANALYZE_NO_STAGE)
-			printf(_("%s: processing database \"%s\": %s\n"),
-				   progname, PQdb(conn), _(stage_messages[stage]));
-		else
-			printf(_("%s: vacuuming database \"%s\"\n"),
-				   progname, PQdb(conn));
-		fflush(stdout);
-	}
+	print_processing_notice(conn, stage, progname, quiet);
 
 	/*
 	 * Prepare the list of tables to process by querying the catalogs.
-- 
2.47.1

From 10b12c50cf784741c763b3a0b6635e8d07b9c713 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Wed, 6 Nov 2024 16:15:16 -0500
Subject: [PATCH v35 09/12] split out generate_catalog_list

---
 src/bin/scripts/vacuumdb.c | 176 +++++++++++++++++++++----------------
 1 file changed, 99 insertions(+), 77 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index e9946f79b2..36f4796db0 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -560,67 +560,38 @@ print_processing_notice(PGconn *conn, int stage, const char *progname, bool quie
 }
 
 /*
- * vacuum_one_database
- *
- * Process tables in the given database.  If the 'tables' list is empty,
- * process all tables in the database.
- *
- * Note that this function is only concerned with running exactly one stage
- * when in analyze-in-stages mode; caller must iterate on us if necessary.
- *
- * If concurrentCons is > 1, multiple connections are used to vacuum tables
- * in parallel.  In this case and if the table list is empty, we first obtain
- * a list of tables from the database.
- */
-static void
-vacuum_one_database(ConnParams *cparams,
-					vacuumingOptions *vacopts,
-					int stage,
-					SimpleStringList *objects,
-					int concurrentCons,
-					const char *progname, bool echo, bool quiet)
+	* Prepare the list of tables to process by querying the catalogs.
+	*
+	* Since we execute the constructed query with the default search_path
+	* (which could be unsafe), everything in this query MUST be fully
+	* qualified.
+	*
+	* First, build a WITH clause for the catalog query if any tables were
+	* specified, with a set of values made of relation names and their
+	* optional set of columns.  This is used to match any provided column
+	* lists with the generated qualified identifiers and to filter for the
+	* tables provided via --table.  If a listed table does not exist, the
+	* catalog query will fail.
+	*/
+static SimpleStringList *
+generate_catalog_list(PGconn *conn,
+					  vacuumingOptions *vacopts,
+					  SimpleStringList *objects,
+					  bool echo,
+					  int *ntups)
 {
-	PQExpBufferData sql;
-	PQExpBufferData buf;
 	PQExpBufferData catalog_query;
-	PGresult   *res;
-	PGconn	   *conn;
+	PQExpBufferData buf;
+	SimpleStringList *dbtables;
 	SimpleStringListCell *cell;
-	ParallelSlotArray *sa;
-	SimpleStringList dbtables = {NULL, NULL};
-	int			i;
-	int			ntups;
-	bool		failed = false;
 	bool		objects_listed = false;
-	const char *initcmd;
-	const char *stage_commands[] = {
-		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
-		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
-		"RESET default_statistics_target;"
-	};
+	PGresult   *res;
+	int			i;
 
-	Assert(stage == ANALYZE_NO_STAGE ||
-		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+	dbtables = palloc(sizeof(SimpleStringList));
+	dbtables->head = NULL;
+	dbtables->tail = NULL;
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	/*
-	 * Prepare the list of tables to process by querying the catalogs.
-	 *
-	 * Since we execute the constructed query with the default search_path
-	 * (which could be unsafe), everything in this query MUST be fully
-	 * qualified.
-	 *
-	 * First, build a WITH clause for the catalog query if any tables were
-	 * specified, with a set of values made of relation names and their
-	 * optional set of columns.  This is used to match any provided column
-	 * lists with the generated qualified identifiers and to filter for the
-	 * tables provided via --table.  If a listed table does not exist, the
-	 * catalog query will fail.
-	 */
 	initPQExpBuffer(&catalog_query);
 	for (cell = objects ? objects->head : NULL; cell; cell = cell->next)
 	{
@@ -771,40 +742,91 @@ vacuum_one_database(ConnParams *cparams,
 	appendPQExpBufferStr(&catalog_query, " ORDER BY c.relpages DESC;");
 	executeCommand(conn, "RESET search_path;", echo);
 	res = executeQuery(conn, catalog_query.data, echo);
+	*ntups = PQntuples(res);
 	termPQExpBuffer(&catalog_query);
 	PQclear(executeQuery(conn, ALWAYS_SECURE_SEARCH_PATH_SQL, echo));
 
-	/*
-	 * If no rows are returned, there are no matching tables, so we are done.
-	 */
-	ntups = PQntuples(res);
-	if (ntups == 0)
-	{
-		PQclear(res);
-		PQfinish(conn);
-		return;
-	}
-
 	/*
 	 * Build qualified identifiers for each table, including the column list
 	 * if given.
 	 */
-	initPQExpBuffer(&buf);
-	for (i = 0; i < ntups; i++)
+	if (*ntups > 0)
 	{
-		appendPQExpBufferStr(&buf,
-							 fmtQualifiedId(PQgetvalue(res, i, 1),
-											PQgetvalue(res, i, 0)));
+		initPQExpBuffer(&buf);
+		for (i = 0; i < *ntups; i++)
+		{
+			appendPQExpBufferStr(&buf,
+								fmtQualifiedId(PQgetvalue(res, i, 1),
+												PQgetvalue(res, i, 0)));
 
-		if (objects_listed && !PQgetisnull(res, i, 2))
-			appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
+			if (objects_listed && !PQgetisnull(res, i, 2))
+				appendPQExpBufferStr(&buf, PQgetvalue(res, i, 2));
 
-		simple_string_list_append(&dbtables, buf.data);
-		resetPQExpBuffer(&buf);
+			simple_string_list_append(dbtables, buf.data);
+			resetPQExpBuffer(&buf);
+		}
+		termPQExpBuffer(&buf);
 	}
-	termPQExpBuffer(&buf);
 	PQclear(res);
 
+	return dbtables;
+}
+
+/*
+ * vacuum_one_database
+ *
+ * Process tables in the given database.  If the 'tables' list is empty,
+ * process all tables in the database.
+ *
+ * Note that this function is only concerned with running exactly one stage
+ * when in analyze-in-stages mode; caller must iterate on us if necessary.
+ *
+ * If concurrentCons is > 1, multiple connections are used to vacuum tables
+ * in parallel.  In this case and if the table list is empty, we first obtain
+ * a list of tables from the database.
+ */
+static void
+vacuum_one_database(ConnParams *cparams,
+					vacuumingOptions *vacopts,
+					int stage,
+					SimpleStringList *objects,
+					int concurrentCons,
+					const char *progname, bool echo, bool quiet)
+{
+	PQExpBufferData sql;
+	PGconn	   *conn;
+	SimpleStringListCell *cell;
+	ParallelSlotArray *sa;
+	int			ntups;
+	bool		failed = false;
+	const char *initcmd;
+	SimpleStringList *dbtables;
+	const char *stage_commands[] = {
+		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
+		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
+		"RESET default_statistics_target;"
+	};
+
+	Assert(stage == ANALYZE_NO_STAGE ||
+		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
+
+	conn = connectDatabase(cparams, progname, echo, false, true);
+
+	check_conn_options(conn, vacopts);
+	print_processing_notice(conn, stage, progname, quiet);
+
+	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
+
+	/*
+	 * If no rows are returned, there are no matching tables, so we are done.
+	 */
+	if (ntups == 0)
+	{
+		PQfinish(conn);
+		return;
+	}
+
+
 	/*
 	 * Ensure concurrentCons is sane.  If there are more connections than
 	 * vacuumable relations, we don't need to use them all.
@@ -837,7 +859,7 @@ vacuum_one_database(ConnParams *cparams,
 
 	initPQExpBuffer(&sql);
 
-	cell = dbtables.head;
+	cell = dbtables->head;
 	do
 	{
 		const char *tabname = cell->val;
-- 
2.47.1

From 7626e26bb4a2fe71a34188e40167dcb5a38cea71 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Fri, 8 Nov 2024 12:27:50 -0500
Subject: [PATCH v35 12/12] Add --force-analyze to vacuumdb.

The vacuumdb options of --analyze-in-stages and --analyze-only are often
used after a restore from a dump or a pg_upgrade to quickly rebuild
stats on a databse.

However, now that stats are imported in most (but not all) cases,
running either of these commands will be at least partially redundant,
and will overwrite the stats that were just imported, which is a big
POLA violation.

We could add a new option such as --analyze-missing-in-stages, but that
wouldn't help the userbase that grown accustomed to running
--analyze-in-stages after an upgrade.

The least-bad option to handle the situation is to change the behavior
of --analyze-only and --analyze-in-stages to only analyze tables which
were missing stats before the vacuumdb started, but offer the
--force-analyze flag to restore the old behavior for those who truly
wanted it.
---
 src/bin/scripts/t/100_vacuumdb.pl |  6 +-
 src/bin/scripts/vacuumdb.c        | 91 ++++++++++++++++++++++++-------
 doc/src/sgml/ref/vacuumdb.sgml    | 48 ++++++++++++++++
 3 files changed, 125 insertions(+), 20 deletions(-)

diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index 1a2bcb4959..2d669391fe 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -127,9 +127,13 @@ $node->issues_sql_like(
 	qr/statement: VACUUM \(SKIP_DATABASE_STATS, ANALYZE\) public.vactable\(a, b\);/,
 	'vacuumdb --analyze with complete column list');
 $node->issues_sql_like(
+	[ 'vacuumdb', '--analyze-only', '--force-analyze', '--table', 'vactable(b)', 'postgres' ],
+	qr/statement: ANALYZE public.vactable\(b\);/,
+	'vacuumdb --analyze-only --force-analyze with partial column list');
+$node->issues_sql_unlike(
 	[ 'vacuumdb', '--analyze-only', '--table', 'vactable(b)', 'postgres' ],
 	qr/statement: ANALYZE public.vactable\(b\);/,
-	'vacuumdb --analyze-only with partial column list');
+	'vacuumdb --analyze-only --force-analyze with partial column list skipping vacuumed tables');
 $node->command_checks_all(
 	[ 'vacuumdb', '--analyze', '--table', 'vacview', 'postgres' ],
 	0,
diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index b13f3c4224..1aa5c46af5 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -25,6 +25,7 @@
 #include "fe_utils/query_utils.h"
 #include "fe_utils/simple_list.h"
 #include "fe_utils/string_utils.h"
+#include "pqexpbuffer.h"
 
 
 /* vacuum options controlled by user flags */
@@ -47,6 +48,8 @@ typedef struct vacuumingOptions
 	bool		process_main;
 	bool		process_toast;
 	bool		skip_database_stats;
+	bool		analyze_in_stages;
+	bool		force_analyze;
 	char	   *buffer_usage_limit;
 } vacuumingOptions;
 
@@ -75,7 +78,6 @@ static void vacuum_one_database(ConnParams *cparams,
 
 static void vacuum_all_databases(ConnParams *cparams,
 								 vacuumingOptions *vacopts,
-								 bool analyze_in_stages,
 								 SimpleStringList *objects,
 								 int concurrentCons,
 								 const char *progname, bool echo, bool quiet);
@@ -140,6 +142,7 @@ main(int argc, char *argv[])
 		{"no-process-toast", no_argument, NULL, 11},
 		{"no-process-main", no_argument, NULL, 12},
 		{"buffer-usage-limit", required_argument, NULL, 13},
+		{"force-analyze", no_argument, NULL, 14},
 		{NULL, 0, NULL, 0}
 	};
 
@@ -156,7 +159,6 @@ main(int argc, char *argv[])
 	bool		echo = false;
 	bool		quiet = false;
 	vacuumingOptions vacopts;
-	bool		analyze_in_stages = false;
 	SimpleStringList objects = {NULL, NULL};
 	int			concurrentCons = 1;
 	int			tbl_count = 0;
@@ -170,6 +172,8 @@ main(int argc, char *argv[])
 	vacopts.do_truncate = true;
 	vacopts.process_main = true;
 	vacopts.process_toast = true;
+	vacopts.force_analyze = false;
+	vacopts.analyze_in_stages = false;
 
 	pg_logging_init(argv[0]);
 	progname = get_progname(argv[0]);
@@ -251,7 +255,7 @@ main(int argc, char *argv[])
 				maintenance_db = pg_strdup(optarg);
 				break;
 			case 3:
-				analyze_in_stages = vacopts.analyze_only = true;
+				vacopts.analyze_in_stages = vacopts.analyze_only = true;
 				break;
 			case 4:
 				vacopts.disable_page_skipping = true;
@@ -287,6 +291,9 @@ main(int argc, char *argv[])
 			case 13:
 				vacopts.buffer_usage_limit = escape_quotes(optarg);
 				break;
+			case 14:
+				vacopts.force_analyze = true;
+				break;
 			default:
 				/* getopt_long already emitted a complaint */
 				pg_log_error_hint("Try \"%s --help\" for more information.", progname);
@@ -372,6 +379,14 @@ main(int argc, char *argv[])
 		pg_fatal("cannot use the \"%s\" option with the \"%s\" option",
 				 "buffer-usage-limit", "full");
 
+	/*
+	 * --force-analyze is only valid when used with --analyze-only, -analyze,
+	 * or --analyze-in-stages
+	 */
+	if (vacopts.force_analyze && !vacopts.analyze_only && !vacopts.analyze_in_stages)
+		pg_fatal("can only use the \"%s\" option with \"%s\" or \"%s\"",
+				 "--force-analyze", "-Z/--analyze-only", "--analyze-in-stages");
+
 	/* fill cparams except for dbname, which is set below */
 	cparams.pghost = host;
 	cparams.pgport = port;
@@ -390,7 +405,6 @@ main(int argc, char *argv[])
 		cparams.dbname = maintenance_db;
 
 		vacuum_all_databases(&cparams, &vacopts,
-							 analyze_in_stages,
 							 &objects,
 							 concurrentCons,
 							 progname, echo, quiet);
@@ -413,20 +427,27 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
-		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+		stage = (vacopts.analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 		conn = connectDatabase(&cparams, progname, echo, false, true);
 		check_conn_options(conn, &vacopts);
 		print_processing_notice(conn, stage, progname, quiet);
 		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
+		vacuum_one_database(&cparams, &vacopts, stage,
+							&objects,
+							concurrentCons,
+							conn,
+							found_objects,
+							ntup,
+							progname, echo, quiet);
 
-		if (analyze_in_stages)
+		if (stage != ANALYZE_NO_STAGE)
 		{
-			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+			for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 			{
 				/* the last pass disconnected the conn */
-				if (stage > 0)
-					conn = connectDatabase(&cparams, progname, echo, false, true);
+				conn = connectDatabase(&cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
@@ -437,14 +458,6 @@ main(int argc, char *argv[])
 									progname, echo, quiet);
 			}
 		}
-		else
-			vacuum_one_database(&cparams, &vacopts, stage,
-								&objects,
-								concurrentCons,
-								conn,
-								found_objects,
-								ntup,
-								progname, echo, quiet);
 	}
 
 	exit(0);
@@ -763,6 +776,47 @@ generate_catalog_list(PGconn *conn,
 						  vacopts->min_mxid_age);
 	}
 
+	/*
+	 * If this query is for an analyze-only or analyze-in-stages, two
+	 * upgrade-centric operations, and force-analyze is NOT set, then
+	 * exclude any relations that already have their full compliment
+	 * of attribute stats and extended stats.
+	 *
+	 *
+	 */
+	if ((vacopts->analyze_only || vacopts->analyze_in_stages) &&
+		!vacopts->force_analyze)
+	{
+		/*
+		 * The pg_class in question has no pg_statistic rows representing
+		 * user-visible columns that lack a corresponding pg_statitic row.
+		 * Currently no differentiation is made for whether the
+		 * pg_statistic.stainherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_attribute AS a\n"
+							 " WHERE a.attrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND a.attnum OPERATOR(pg_catalog.>) 0 AND NOT a.attisdropped\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic AS s\n"
+							 " WHERE s.starelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND s.staattnum OPERATOR(pg_catalog.=) a.attnum))\n");
+
+		/*
+		 * The pg_class entry has no pg_statistic_ext rows that lack a corresponding
+		 * pg_statistic_ext_data row. Currently no differentiation is made for whether
+		 * pg_statistic_exta_data.stxdinherit is true or false.
+		 */
+		appendPQExpBufferStr(&catalog_query,
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext AS e\n"
+							 " WHERE e.stxrelid OPERATOR(pg_catalog.=) c.oid\n"
+							 " AND NOT EXISTS (\n"
+							 " SELECT NULL FROM pg_catalog.pg_statistic_ext_data AS d\n"
+							 " WHERE d.stxoid OPERATOR(pg_catalog.=) e.oid))\n");
+	}
+
 	/*
 	 * Execute the catalog query.  We use the default search_path for this
 	 * query for consistency with table lookups done elsewhere by the user.
@@ -959,7 +1013,6 @@ finish:
 static void
 vacuum_all_databases(ConnParams *cparams,
 					 vacuumingOptions *vacopts,
-					 bool analyze_in_stages,
 					 SimpleStringList *objects,
 					 int concurrentCons,
 					 const char *progname, bool echo, bool quiet)
@@ -972,7 +1025,7 @@ vacuum_all_databases(ConnParams *cparams,
 	SimpleStringList  **found_objects;
 	int				   *num_tuples;
 
-	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+	stage = (vacopts->analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
diff --git a/doc/src/sgml/ref/vacuumdb.sgml b/doc/src/sgml/ref/vacuumdb.sgml
index 66fccb30a2..00ec927606 100644
--- a/doc/src/sgml/ref/vacuumdb.sgml
+++ b/doc/src/sgml/ref/vacuumdb.sgml
@@ -425,6 +425,12 @@ PostgreSQL documentation
        <para>
         Only calculate statistics for use by the optimizer (no vacuum).
        </para>
+       <para>
+        By default, this operation excludes relations that already have
+        statistics generated. If the option <option>--force-analyze</option>
+        is also specified, then relations with existing stastistics are not
+        excluded.
+       </para>
       </listitem>
      </varlistentry>
 
@@ -439,6 +445,47 @@ PostgreSQL documentation
         to produce usable statistics faster, and subsequent stages build the
         full statistics.
        </para>
+       <para>
+        This option is intended to be run after a <command>pg_upgrade</command>
+        to generate statistics for relations that have no stistatics or incomplete
+        statistics (such as those with extended statistics objects, which are not
+        imported on upgrade).
+       </para>
+       <para>
+        If the option <option>--force-analyze</option> is also specified, then
+        relations with existing stastistics are not excluded.
+        This option is only useful to analyze a database that currently has
+        no statistics or has wholly incorrect ones, such as if it is newly
+        populated from a restored dump.
+        Be aware that running with this option combinationin a database with
+        existing statistics may cause the query optimizer choices to become
+        transiently worse due to the low statistics targets of the early
+        stages.
+       </para>
+      </listitem>
+     </varlistentry>
+
+     <varlistentry>
+      <term><option>--force-analyze</option></term>
+      <listitem>
+       <para>
+        This option can only be used if either <option>--analyze-only</option>
+        or <option>--analyze-in-stages</option> is specified. It modifies those
+        options to not filter out relations that already have statistics.
+       </para>
+       <para>
+
+        Only calculate statistics for use by the optimizer (no vacuum),
+        like <option>--analyze-only</option>.  Run three
+        stages of analyze; the first stage uses the lowest possible statistics
+        target (see <xref linkend="guc-default-statistics-target"/>)
+        to produce usable statistics faster, and subsequent stages build the
+        full statistics.
+       </para>
+
+       <para>
+        This option was created
+       </para>
 
        <para>
         This option is only useful to analyze a database that currently has
@@ -452,6 +499,7 @@ PostgreSQL documentation
       </listitem>
      </varlistentry>
 
+
      <varlistentry>
        <term><option>-?</option></term>
        <term><option>--help</option></term>
-- 
2.47.1

From 4fd29dd1ff736b6508f85e63273c0fb4fdde8625 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Thu, 7 Nov 2024 18:06:52 -0500
Subject: [PATCH v35 11/12] Add issues_sql_unlike, opposite of issues_sql_like

This is the same as issues_sql_like(), but the specified text is
prohibited from being in the output rather than required.

This became necessary to test that a command-line filter was in fact
filtering out certain output that a prior test required.
---
 src/test/perl/PostgreSQL/Test/Cluster.pm | 25 ++++++++++++++++++++++++
 1 file changed, 25 insertions(+)

diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 508e5e3917..1281071479 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -2799,6 +2799,31 @@ sub issues_sql_like
 }
 
 =pod
+=item $node->issues_sql_unlike(cmd, prohibited_sql, test_name)
+
+Run a command on the node, then verify that $prohibited_sql does not appear
+in the server log file.
+
+=cut
+
+sub issues_sql_unlike
+{
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	my ($self, $cmd, $prohibited_sql, $test_name) = @_;
+
+	local %ENV = $self->_get_env();
+
+	my $log_location = -s $self->logfile;
+
+	my $result = PostgreSQL::Test::Utils::run_log($cmd);
+	ok($result, "@$cmd exit code 0");
+	my $log =
+	  PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+	unlike($log, $prohibited_sql, "$test_name: SQL found in server log");
+	return;
+}
+
 
 =item $node->log_content()
 
-- 
2.47.1

From c4b6a5d869f255ee9f5d853a25cdb2ee225c9193 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Wed, 6 Nov 2024 21:30:49 -0500
Subject: [PATCH v35 10/12] preserve catalog lists across staged runs

---
 src/bin/scripts/vacuumdb.c | 113 ++++++++++++++++++++++++++-----------
 1 file changed, 80 insertions(+), 33 deletions(-)

diff --git a/src/bin/scripts/vacuumdb.c b/src/bin/scripts/vacuumdb.c
index 36f4796db0..b13f3c4224 100644
--- a/src/bin/scripts/vacuumdb.c
+++ b/src/bin/scripts/vacuumdb.c
@@ -68,6 +68,9 @@ static void vacuum_one_database(ConnParams *cparams,
 								int stage,
 								SimpleStringList *objects,
 								int concurrentCons,
+								PGconn *conn,
+								SimpleStringList *dbtables,
+								int ntups,
 								const char *progname, bool echo, bool quiet);
 
 static void vacuum_all_databases(ConnParams *cparams,
@@ -83,6 +86,14 @@ static void prepare_vacuum_command(PQExpBuffer sql, int serverVersion,
 static void run_vacuum_command(PGconn *conn, const char *sql, bool echo,
 							   const char *table);
 
+static void check_conn_options(PGconn *conn, vacuumingOptions *vacopts);
+
+static void
+print_processing_notice(PGconn *conn, int stage, const char *progname, bool quiet);
+
+static SimpleStringList * generate_catalog_list(PGconn *conn, vacuumingOptions *vacopts,
+												SimpleStringList *objects, bool echo, int *ntups);
+
 static void help(const char *progname);
 
 void		check_objfilter(void);
@@ -386,6 +397,11 @@ main(int argc, char *argv[])
 	}
 	else
 	{
+		PGconn	   *conn;
+		int			ntup;
+		SimpleStringList   *found_objects;
+		int			stage;
+
 		if (dbname == NULL)
 		{
 			if (getenv("PGDATABASE"))
@@ -397,25 +413,37 @@ main(int argc, char *argv[])
 		}
 
 		cparams.dbname = dbname;
+		stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
+
+		conn = connectDatabase(&cparams, progname, echo, false, true);
+		check_conn_options(conn, &vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects = generate_catalog_list(conn, &vacopts, &objects, echo, &ntup);
 
 		if (analyze_in_stages)
 		{
-			int			stage;
-
 			for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
 			{
-				vacuum_one_database(&cparams, &vacopts,
-									stage,
+				/* the last pass disconnected the conn */
+				if (stage > 0)
+					conn = connectDatabase(&cparams, progname, echo, false, true);
+
+				vacuum_one_database(&cparams, &vacopts, stage,
 									&objects,
 									concurrentCons,
+									conn,
+									found_objects,
+									ntup,
 									progname, echo, quiet);
 			}
 		}
 		else
-			vacuum_one_database(&cparams, &vacopts,
-								ANALYZE_NO_STAGE,
+			vacuum_one_database(&cparams, &vacopts, stage,
 								&objects,
 								concurrentCons,
+								conn,
+								found_objects,
+								ntup,
 								progname, echo, quiet);
 	}
 
@@ -791,16 +819,17 @@ vacuum_one_database(ConnParams *cparams,
 					int stage,
 					SimpleStringList *objects,
 					int concurrentCons,
+					PGconn *conn,
+					SimpleStringList *dbtables,
+					int ntups,
 					const char *progname, bool echo, bool quiet)
 {
 	PQExpBufferData sql;
-	PGconn	   *conn;
 	SimpleStringListCell *cell;
 	ParallelSlotArray *sa;
-	int			ntups;
 	bool		failed = false;
 	const char *initcmd;
-	SimpleStringList *dbtables;
+
 	const char *stage_commands[] = {
 		"SET default_statistics_target=1; SET vacuum_cost_delay=0;",
 		"SET default_statistics_target=10; RESET vacuum_cost_delay;",
@@ -810,13 +839,6 @@ vacuum_one_database(ConnParams *cparams,
 	Assert(stage == ANALYZE_NO_STAGE ||
 		   (stage >= 0 && stage < ANALYZE_NUM_STAGES));
 
-	conn = connectDatabase(cparams, progname, echo, false, true);
-
-	check_conn_options(conn, vacopts);
-	print_processing_notice(conn, stage, progname, quiet);
-
-	dbtables = generate_catalog_list(conn, vacopts, objects, echo, &ntups);
-
 	/*
 	 * If no rows are returned, there are no matching tables, so we are done.
 	 */
@@ -928,7 +950,7 @@ finish:
 }
 
 /*
- * Vacuum/analyze all connectable databases.
+ * Vacuum/analyze all ccparams->override_dbname = PQgetvalue(result, i, 0);onnectable databases.
  *
  * In analyze-in-stages mode, we process all databases in one stage before
  * moving on to the next stage.  That ensure minimal stats are available
@@ -944,8 +966,13 @@ vacuum_all_databases(ConnParams *cparams,
 {
 	PGconn	   *conn;
 	PGresult   *result;
-	int			stage;
 	int			i;
+	int			stage;
+
+	SimpleStringList  **found_objects;
+	int				   *num_tuples;
+
+	stage = (analyze_in_stages) ? 0 : ANALYZE_NO_STAGE;
 
 	conn = connectMaintenanceDatabase(cparams, progname, echo);
 	result = executeQuery(conn,
@@ -953,7 +980,33 @@ vacuum_all_databases(ConnParams *cparams,
 						  echo);
 	PQfinish(conn);
 
-	if (analyze_in_stages)
+	/*
+	 * connect to each database, check validity of options,
+	 * build the list of found objects per database,
+	 * and run the first/only vacuum stage
+	 */
+	found_objects = palloc(PQntuples(result) * sizeof(SimpleStringList *));
+	num_tuples = palloc(PQntuples(result) * sizeof (int));
+
+	for (i = 0; i < PQntuples(result); i++)
+	{
+		cparams->override_dbname = PQgetvalue(result, i, 0);
+		conn = connectDatabase(cparams, progname, echo, false, true);
+		check_conn_options(conn, vacopts);
+		print_processing_notice(conn, stage, progname, quiet);
+		found_objects[i] = generate_catalog_list(conn, vacopts, objects, echo, &num_tuples[i]);
+
+		vacuum_one_database(cparams, vacopts,
+							stage,
+							objects,
+							concurrentCons,
+							conn,
+							found_objects[i],
+							num_tuples[i],
+							progname, echo, quiet);
+	}
+
+	if (stage != ANALYZE_NO_STAGE)
 	{
 		/*
 		 * When analyzing all databases in stages, we analyze them all in the
@@ -963,35 +1016,29 @@ vacuum_all_databases(ConnParams *cparams,
 		 * This means we establish several times as many connections, but
 		 * that's a secondary consideration.
 		 */
-		for (stage = 0; stage < ANALYZE_NUM_STAGES; stage++)
+		for (stage = 1; stage < ANALYZE_NUM_STAGES; stage++)
 		{
 			for (i = 0; i < PQntuples(result); i++)
 			{
 				cparams->override_dbname = PQgetvalue(result, i, 0);
+				conn = connectDatabase(cparams, progname, echo, false, true);
+				print_processing_notice(conn, stage, progname, quiet);
 
 				vacuum_one_database(cparams, vacopts,
 									stage,
 									objects,
 									concurrentCons,
+									conn,
+									found_objects[i],
+									num_tuples[i],
 									progname, echo, quiet);
 			}
 		}
 	}
-	else
-	{
-		for (i = 0; i < PQntuples(result); i++)
-		{
-			cparams->override_dbname = PQgetvalue(result, i, 0);
-
-			vacuum_one_database(cparams, vacopts,
-								ANALYZE_NO_STAGE,
-								objects,
-								concurrentCons,
-								progname, echo, quiet);
-		}
-	}
 
 	PQclear(result);
+	pfree(found_objects);
+	pfree(num_tuples);
 }
 
 /*
-- 
2.47.1

Reply via email to