>
>
>
> I'll be rebasing (that's done) and refactoring 0003 to get rid of the
> positional column, and moving 0014 next to 0003 because they touch the same
> files.
>

As promised, rebased (as of 8fcd80258bcf43dab93d877a5de0ce3f4d2bd471)

Things have been reordered here in a mostly-priority order:

0001 - Enable dumping stats in pg_dump/pg_upgrade. Now with less dead code!
0002 - The dump --no-data option. Very handy for diagnosing customer query
plans, but not in the critical path for v18. Still, so useful that we ought
to include it.
0003 - Re-enabling in-place updates because catalog bloat bad.
0004 - Combine two syscache lookups into one. Not strictly necessary, but
the second lookup is redundant if the first one grabs what we need.
0005-0010 vacuumdb changes (still up for debate) in baby steps.
0011 - The fix for clearing relation stats if the default for relpages
becomes -1 on inherited tables (which it hasn't, yet, and might never).
From 0f616d4d44f7e29517ac348ded09da1fef1bcbd9 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 v33 02/11] Add --no-data option.

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

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

From 4538ce000c39c53ec23bfbe9c903f5e47d28f573 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Mon, 4 Nov 2024 14:02:57 -0500
Subject: [PATCH v33 03/11] Enable in-place updates for
 pg_restore_relation_stats.

This matches the behavior of the ANALYZE command, and would avoid
bloating pg_class in an upgrade situation wherein
pg_restore_relation_stats would be called for nearly every relation in
the database.
---
 src/backend/statistics/relation_stats.c | 72 +++++++++++++++++++------
 1 file changed, 55 insertions(+), 17 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index e619d5cf5b..24f646048c 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -22,6 +22,7 @@
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
+#include "utils/fmgroids.h"
 
 #define DEFAULT_RELPAGES Int32GetDatum(0)
 #define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
@@ -50,13 +51,14 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel);
+static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
+									   bool inplace);
 
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -68,6 +70,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	int			ncols = 0;
 	TupleDesc	tupdesc;
 	bool		result = true;
+	void	   *inplace_state;
 
 	stats_check_required_arg(fcinfo, relarginfo, RELATION_ARG);
 	reloid = PG_GETARG_OID(RELATION_ARG);
@@ -87,7 +90,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	crel = table_open(RelationRelationId, RowExclusiveLock);
 
 	tupdesc = RelationGetDescr(crel);
-	ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+	if (inplace)
+	{
+		ScanKeyData key[1];
+
+		ctup = NULL;
+
+		ScanKeyInit(&key[0], Anum_pg_class_oid, BTEqualStrategyNumber, F_OIDEQ,
+					ObjectIdGetDatum(reloid));
+		systable_inplace_update_begin(crel, ClassOidIndexId, true, NULL, 1, key,
+									  &ctup, &inplace_state);
+	}
+	else
+		ctup = SearchSysCacheCopy1(RELOID, ObjectIdGetDatum(reloid));
+
 	if (!HeapTupleIsValid(ctup))
 	{
 		ereport(elevel,
@@ -118,8 +134,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relpages != pgcform->relpages)
 		{
-			replaces[ncols] = Anum_pg_class_relpages;
-			values[ncols] = Int32GetDatum(relpages);
+			if (inplace)
+				pgcform->relpages = relpages;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relpages;
+				values[ncols] = Int32GetDatum(relpages);
+			}
 			ncols++;
 		}
 	}
@@ -137,8 +158,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (reltuples != pgcform->reltuples)
 		{
-			replaces[ncols] = Anum_pg_class_reltuples;
-			values[ncols] = Float4GetDatum(reltuples);
+			if (inplace)
+				pgcform->reltuples = reltuples;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_reltuples;
+				values[ncols] = Float4GetDatum(reltuples);
+			}
 			ncols++;
 		}
 
@@ -157,8 +183,13 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 		}
 		else if (relallvisible != pgcform->relallvisible)
 		{
-			replaces[ncols] = Anum_pg_class_relallvisible;
-			values[ncols] = Int32GetDatum(relallvisible);
+			if (inplace)
+				pgcform->relallvisible = relallvisible;
+			else
+			{
+				replaces[ncols] = Anum_pg_class_relallvisible;
+				values[ncols] = Int32GetDatum(relallvisible);
+			}
 			ncols++;
 		}
 	}
@@ -166,13 +197,20 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 	/* only update pg_class if there is a meaningful change */
 	if (ncols > 0)
 	{
-		HeapTuple	newtup;
+		if (inplace)
+			systable_inplace_update_finish(inplace_state, ctup);
+		else
+		{
+			HeapTuple	newtup;
 
-		newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
-										   nulls);
-		CatalogTupleUpdate(crel, &newtup->t_self, newtup);
-		heap_freetuple(newtup);
+			newtup = heap_modify_tuple_by_cols(ctup, tupdesc, ncols, replaces, values,
+											nulls);
+			CatalogTupleUpdate(crel, &newtup->t_self, newtup);
+			heap_freetuple(newtup);
+		}
 	}
+	else if (inplace)
+		systable_inplace_update_cancel(inplace_state);
 
 	/* release the lock, consistent with vac_update_relstats() */
 	table_close(crel, RowExclusiveLock);
@@ -188,7 +226,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR);
+	relation_statistics_update(fcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -212,7 +250,7 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
 	newfcinfo->args[3].isnull = false;
 
-	relation_statistics_update(newfcinfo, ERROR);
+	relation_statistics_update(newfcinfo, ERROR, false);
 	PG_RETURN_VOID();
 }
 
@@ -230,7 +268,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

From 0d01cf8eece54da540f221e7d3e93279290fd573 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 v33 05/11] split out check_conn_options

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

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

From 789830b90b4b4ac7edaa59555fd80b5e7c1fa5ee 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 v33 01/11] Enable dumping of table/index stats in pg_dump.

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

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

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

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

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

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

From 0750f8c4fa8d5144857d7dfe2117d1414cc10b56 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 v33 04/11] Consolidate attribute syscache lookups into one
 call by name.

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

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

From 041dd15f564088ed6b2df7885af7ffdcd71cb554 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 v33 06/11] split out print_processing_notice

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

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

From ccb3412a776e5df2dae1f3f1d2b7f708b310f2af 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 v33 07/11] split out generate_catalog_list

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

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

From e924cc42fa8e2ae3e479f125cfaae2d82c193dbf 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 v33 09/11] Add issues_sql_unlike, opposite of issues_sql_like

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

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

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

From 975dd88313e88ae8d579b1e0cd68d7944cb6a391 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 v33 08/11] preserve catalog lists across staged runs

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

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

From ae94d0a1f6af01a0816d9515d6b03d1de9e0596e 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 v33 10/11] Add --force-analyze to vacuumdb.

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

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

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

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

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

From 8dafe8543f0c5e6204f8f8161e6c349b6a022d33 Mon Sep 17 00:00:00 2001
From: Corey Huinker <corey.huin...@gmail.com>
Date: Mon, 4 Nov 2024 16:21:39 -0500
Subject: [PATCH v33 11/11] Enable pg_clear_relation_stats to handle different
 default relpages.

If it comes to pass that relpages has the default of -1.0 for
partitioned tables (and indexes), then this patch will handle that.
---
 src/backend/statistics/relation_stats.c | 47 +++++++++++++------------
 1 file changed, 24 insertions(+), 23 deletions(-)

diff --git a/src/backend/statistics/relation_stats.c b/src/backend/statistics/relation_stats.c
index 24f646048c..e27fe79406 100644
--- a/src/backend/statistics/relation_stats.c
+++ b/src/backend/statistics/relation_stats.c
@@ -19,15 +19,12 @@
 
 #include "access/heapam.h"
 #include "catalog/indexing.h"
+#include "catalog/pg_class_d.h"
 #include "statistics/stat_utils.h"
 #include "utils/fmgrprotos.h"
 #include "utils/syscache.h"
 #include "utils/fmgroids.h"
 
-#define DEFAULT_RELPAGES Int32GetDatum(0)
-#define DEFAULT_RELTUPLES Float4GetDatum(-1.0)
-#define DEFAULT_RELALLVISIBLE Int32GetDatum(0)
-
 /*
  * Positional argument numbers, names, and types for
  * relation_statistics_update().
@@ -51,14 +48,11 @@ static struct StatsArgInfo relarginfo[] =
 	[NUM_RELATION_STATS_ARGS] = {0}
 };
 
-static bool relation_statistics_update(FunctionCallInfo fcinfo, int elevel,
-									   bool inplace);
-
 /*
  * Internal function for modifying statistics for a relation.
  */
 static bool
-relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
+relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace, bool clear)
 {
 	Oid			reloid;
 	Relation	crel;
@@ -116,9 +110,19 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 	pgcform = (Form_pg_class) GETSTRUCT(ctup);
 
 	/* relpages */
-	if (!PG_ARGISNULL(RELPAGES_ARG))
+	if (!PG_ARGISNULL(RELPAGES_ARG) || clear)
 	{
-		int32		relpages = PG_GETARG_INT32(RELPAGES_ARG);
+		int32		relpages;
+
+		if (clear)
+			/* relpages default varies by relkind */
+			if ((crel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE) ||
+				(crel->rd_rel->relkind == RELKIND_PARTITIONED_INDEX))
+				relpages = -1;
+			else
+				relpages = 0;
+		else
+			relpages = PG_GETARG_INT32(RELPAGES_ARG);
 
 		/*
 		 * Partitioned tables may have relpages=-1. Note: for relations with
@@ -145,9 +149,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 		}
 	}
 
-	if (!PG_ARGISNULL(RELTUPLES_ARG))
+	if (!PG_ARGISNULL(RELTUPLES_ARG) || clear)
 	{
-		float		reltuples = PG_GETARG_FLOAT4(RELTUPLES_ARG);
+		float		reltuples = (clear) ? -1.0 : PG_GETARG_FLOAT4(RELTUPLES_ARG);
 
 		if (reltuples < -1.0)
 		{
@@ -170,9 +174,9 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 
 	}
 
-	if (!PG_ARGISNULL(RELALLVISIBLE_ARG))
+	if (!PG_ARGISNULL(RELALLVISIBLE_ARG) || clear)
 	{
-		int32		relallvisible = PG_GETARG_INT32(RELALLVISIBLE_ARG);
+		int32		relallvisible = (clear) ? 0 : PG_GETARG_INT32(RELALLVISIBLE_ARG);
 
 		if (relallvisible < 0)
 		{
@@ -226,7 +230,7 @@ relation_statistics_update(FunctionCallInfo fcinfo, int elevel, bool inplace)
 Datum
 pg_set_relation_stats(PG_FUNCTION_ARGS)
 {
-	relation_statistics_update(fcinfo, ERROR, false);
+	relation_statistics_update(fcinfo, ERROR, false, false);
 	PG_RETURN_VOID();
 }
 
@@ -243,14 +247,11 @@ pg_clear_relation_stats(PG_FUNCTION_ARGS)
 
 	newfcinfo->args[0].value = PG_GETARG_OID(0);
 	newfcinfo->args[0].isnull = PG_ARGISNULL(0);
-	newfcinfo->args[1].value = DEFAULT_RELPAGES;
-	newfcinfo->args[1].isnull = false;
-	newfcinfo->args[2].value = DEFAULT_RELTUPLES;
-	newfcinfo->args[2].isnull = false;
-	newfcinfo->args[3].value = DEFAULT_RELALLVISIBLE;
-	newfcinfo->args[3].isnull = false;
+	newfcinfo->args[1].isnull = true;
+	newfcinfo->args[2].isnull = true;
+	newfcinfo->args[3].isnull = true;
 
-	relation_statistics_update(newfcinfo, ERROR, false);
+	relation_statistics_update(newfcinfo, ERROR, false, true);
 	PG_RETURN_VOID();
 }
 
@@ -268,7 +269,7 @@ pg_restore_relation_stats(PG_FUNCTION_ARGS)
 										  relarginfo, WARNING))
 		result = false;
 
-	if (!relation_statistics_update(positional_fcinfo, WARNING, true))
+	if (!relation_statistics_update(positional_fcinfo, WARNING, true, false))
 		result = false;
 
 	PG_RETURN_BOOL(result);
-- 
2.47.0

Reply via email to