diff --git a/contrib/postgres_fdw/deparse.c b/contrib/postgres_fdw/deparse.c
index c159ecd1558..2dcc6c8af1b 100644
--- a/contrib/postgres_fdw/deparse.c
+++ b/contrib/postgres_fdw/deparse.c
@@ -2541,8 +2541,8 @@ deparseAnalyzeSizeSql(StringInfo buf, Relation rel)
 }
 
 /*
- * Construct SELECT statement to acquire the number of rows and the relkind of
- * a relation.
+ * Construct SELECT statement to acquire the number of pages, the number of
+ * rows, and the relkind of a relation.
  *
  * Note: we just return the remote server's reltuples value, which might
  * be off a good deal, but it doesn't seem worth working harder.  See
@@ -2557,7 +2557,7 @@ deparseAnalyzeInfoSql(StringInfo buf, Relation rel)
 	initStringInfo(&relname);
 	deparseRelation(&relname, rel);
 
-	appendStringInfoString(buf, "SELECT reltuples, relkind FROM pg_catalog.pg_class WHERE oid = ");
+	appendStringInfoString(buf, "SELECT relpages, reltuples, relkind FROM pg_catalog.pg_class WHERE oid = ");
 	deparseStringLiteral(buf, relname.data);
 	appendStringInfoString(buf, "::pg_catalog.regclass");
 }
diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out
index cd22553236f..10e87acabef 100644
--- a/contrib/postgres_fdw/expected/postgres_fdw.out
+++ b/contrib/postgres_fdw/expected/postgres_fdw.out
@@ -12863,6 +12863,66 @@ ANALYZE analyze_ftable;
 DROP FOREIGN TABLE analyze_ftable;
 DROP TABLE analyze_table;
 -- ===================================================================
+-- test for statistics import
+-- ===================================================================
+CREATE TABLE simport_table (c1 int, c2 text);
+CREATE FOREIGN TABLE simport_ftable (c1 int, c2 text, cx int)
+       SERVER loopback OPTIONS (table_name 'simport_table');
+ALTER FOREIGN TABLE simport_ftable ALTER COLUMN cx OPTIONS (ADD column_name 'c1');
+ALTER FOREIGN TABLE simport_ftable OPTIONS (ADD restore_stats 'true');
+ANALYZE simport_ftable;                   -- should fail
+WARNING:  could not import statistics for foreign table "public.simport_ftable" --- remote table "public.simport_table" has no relation statistics to import
+ANALYZE simport_table;
+ANALYZE VERBOSE simport_ftable;           -- should work
+INFO:  importing statistics for foreign table "public.simport_ftable"
+INFO:  finished importing statistics for foreign table "public.simport_ftable"
+ALTER TABLE simport_table ALTER COLUMN c1 SET STATISTICS 0;
+ALTER TABLE simport_table ALTER COLUMN c2 SET STATISTICS 0;
+INSERT INTO simport_table VALUES (1, 'foo'), (1, 'foo'), (2, 'bar'), (2, 'bar');
+ANALYZE simport_table;
+ANALYZE simport_ftable;                   -- should fail
+WARNING:  could not import statistics for foreign table "public.simport_ftable" --- remote table "public.simport_table" has no attribute statistics to import
+ALTER TABLE simport_table ALTER COLUMN c1 SET STATISTICS DEFAULT;
+ANALYZE simport_table;
+ANALYZE simport_ftable;                   -- should fail
+WARNING:  could not import statistics for foreign table "public.simport_ftable" --- no attribute statistics found for column "c2" of remote table "public.simport_table"
+ALTER TABLE simport_table ALTER COLUMN c2 SET STATISTICS DEFAULT;
+ANALYZE simport_table;
+ANALYZE VERBOSE simport_ftable;           -- should work
+INFO:  importing statistics for foreign table "public.simport_ftable"
+INFO:  finished importing statistics for foreign table "public.simport_ftable"
+ANALYZE VERBOSE simport_ftable (c1);      -- should work
+INFO:  importing statistics for foreign table "public.simport_ftable"
+INFO:  finished importing statistics for foreign table "public.simport_ftable"
+ANALYZE VERBOSE simport_ftable (c2);      -- should work
+INFO:  importing statistics for foreign table "public.simport_ftable"
+INFO:  finished importing statistics for foreign table "public.simport_ftable"
+ANALYZE VERBOSE simport_ftable (c1, cx);  -- should work
+INFO:  importing statistics for foreign table "public.simport_ftable"
+INFO:  finished importing statistics for foreign table "public.simport_ftable"
+ANALYZE VERBOSE simport_ftable (c2, cx);  -- should work
+INFO:  importing statistics for foreign table "public.simport_ftable"
+INFO:  finished importing statistics for foreign table "public.simport_ftable"
+CREATE STATISTICS stats (dependencies) ON c1, c2 FROM simport_ftable;
+ANALYZE simport_ftable;                   -- should fail
+WARNING:  cannot import statistics for foreign table "public.simport_ftable" --- this foreign table has extended statistics objects
+DROP STATISTICS stats;
+ANALYZE simport_ftable (cid);             -- should fail
+ERROR:  column "cid" of relation "simport_ftable" does not exist
+ANALYZE simport_ftable (c1, c1);          -- should fail
+ERROR:  column "c1" of relation "simport_ftable" appears more than once
+CREATE VIEW simport_view AS SELECT * FROM simport_table;
+CREATE FOREIGN TABLE simport_fview (c1 int, c2 text)
+       SERVER loopback OPTIONS (table_name 'simport_view');
+ALTER FOREIGN TABLE simport_fview OPTIONS (ADD restore_stats 'true');
+ANALYZE simport_fview;                    -- should fail
+WARNING:  could not import statistics for foreign table "public.simport_fview" --- remote table "public.simport_view" is of relkind "v" which cannot have statistics
+-- cleanup
+DROP FOREIGN TABLE simport_ftable;
+DROP FOREIGN TABLE simport_fview;
+DROP VIEW simport_view;
+DROP TABLE simport_table;
+-- ===================================================================
 -- test for postgres_fdw_get_connections function with check_conn = true
 -- ===================================================================
 -- Disable debug_discard_caches in order to manage remote connections
diff --git a/contrib/postgres_fdw/option.c b/contrib/postgres_fdw/option.c
index b0bd72d1e58..3944aedbacc 100644
--- a/contrib/postgres_fdw/option.c
+++ b/contrib/postgres_fdw/option.c
@@ -120,7 +120,8 @@ postgres_fdw_validator(PG_FUNCTION_ARGS)
 			strcmp(def->defname, "async_capable") == 0 ||
 			strcmp(def->defname, "parallel_commit") == 0 ||
 			strcmp(def->defname, "parallel_abort") == 0 ||
-			strcmp(def->defname, "keep_connections") == 0)
+			strcmp(def->defname, "keep_connections") == 0 ||
+			strcmp(def->defname, "restore_stats") == 0)
 		{
 			/* these accept only boolean values */
 			(void) defGetBoolean(def);
@@ -274,6 +275,9 @@ InitPgFdwOptions(void)
 		/* sampling is available on both server and table */
 		{"analyze_sampling", ForeignServerRelationId, false},
 		{"analyze_sampling", ForeignTableRelationId, false},
+		/* restore_stats is available on both server and table */
+		{"restore_stats", ForeignServerRelationId, false},
+		{"restore_stats", ForeignTableRelationId, false},
 
 		{"use_scram_passthrough", ForeignServerRelationId, false},
 		{"use_scram_passthrough", UserMappingRelationId, false},
diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c
index cc8ec24c30e..267cd44fa89 100644
--- a/contrib/postgres_fdw/postgres_fdw.c
+++ b/contrib/postgres_fdw/postgres_fdw.c
@@ -21,8 +21,10 @@
 #include "commands/defrem.h"
 #include "commands/explain_format.h"
 #include "commands/explain_state.h"
+#include "commands/vacuum.h"
 #include "executor/execAsync.h"
 #include "executor/instrument.h"
+#include "executor/spi.h"
 #include "foreign/fdwapi.h"
 #include "funcapi.h"
 #include "miscadmin.h"
@@ -40,6 +42,7 @@
 #include "optimizer/tlist.h"
 #include "parser/parsetree.h"
 #include "postgres_fdw.h"
+#include "statistics/statistics.h"
 #include "storage/latch.h"
 #include "utils/builtins.h"
 #include "utils/float.h"
@@ -318,6 +321,182 @@ typedef struct
 	List	   *already_used;	/* expressions already dealt with */
 } ec_member_foreign_arg;
 
+/* Result sets that are returned from a foreign statistics scan */
+typedef struct
+{
+	PGresult   *rel;
+	PGresult   *att;
+	int			server_version_num;
+} RemoteStatsResults;
+
+/* Pairs of remote columns with local columns */
+typedef struct
+{
+	AttrNumber	local_attnum;
+	char		local_attname[NAMEDATALEN];
+	char		remote_attname[NAMEDATALEN];
+	int			res_index;
+} RemoteAttributeMapping;
+
+/* Column order in relation stats query */
+enum RelStatsColumns
+{
+	RELSTATS_RELPAGES = 0,
+	RELSTATS_RELTUPLES,
+	RELSTATS_RELKIND,
+	RELSTATS_NUM_FIELDS,
+};
+
+/* Column order in attribute stats query */
+enum AttStatsColumns
+{
+	ATTSTATS_ATTNAME = 0,
+	ATTSTATS_NULL_FRAC,
+	ATTSTATS_AVG_WIDTH,
+	ATTSTATS_N_DISTINCT,
+	ATTSTATS_MOST_COMMON_VALS,
+	ATTSTATS_MOST_COMMON_FREQS,
+	ATTSTATS_HISTOGRAM_BOUNDS,
+	ATTSTATS_CORRELATION,
+	ATTSTATS_MOST_COMMON_ELEMS,
+	ATTSTATS_MOST_COMMON_ELEM_FREQS,
+	ATTSTATS_ELEM_COUNT_HISTOGRAM,
+	ATTSTATS_RANGE_LENGTH_HISTOGRAM,
+	ATTSTATS_RANGE_EMPTY_FRAC,
+	ATTSTATS_RANGE_BOUNDS_HISTOGRAM,
+	ATTSTATS_NUM_FIELDS,
+};
+
+/* Relation stats import query */
+static const char *relimport_sql =
+	"SELECT pg_catalog.pg_restore_relation_stats(\n"
+	"\t'version', $1,\n"
+	"\t'schemaname', $2,\n"
+	"\t'relname', $3,\n"
+	"\t'relpages', $4::integer,\n"
+	"\t'reltuples', $5::real)";
+
+/* Argument order in relation stats import query */
+enum RelImportSqlArgs
+{
+	RELIMPORT_SQL_VERSION = 0,
+	RELIMPORT_SQL_SCHEMANAME,
+	RELIMPORT_SQL_RELNAME,
+	RELIMPORT_SQL_RELPAGES,
+	RELIMPORT_SQL_RELTUPLES,
+	RELIMPORT_SQL_NUM_FIELDS
+};
+
+/* Argument types in relation stats import query */
+static const Oid relimport_argtypes[RELIMPORT_SQL_NUM_FIELDS] =
+{
+	INT4OID, TEXTOID, TEXTOID, TEXTOID,
+	TEXTOID,
+};
+
+/* Attribute stats import query */
+static const char *attimport_sql =
+	"SELECT pg_catalog.pg_restore_attribute_stats(\n"
+	"\t'version', $1,\n"
+	"\t'schemaname', $2,\n"
+	"\t'relname', $3,\n"
+	"\t'attnum', $4,\n"
+	"\t'inherited', false::boolean,\n"
+	"\t'null_frac', $5::real,\n"
+	"\t'avg_width', $6::integer,\n"
+	"\t'n_distinct', $7::real,\n"
+	"\t'most_common_vals', $8,\n"
+	"\t'most_common_freqs', $9::real[],\n"
+	"\t'histogram_bounds', $10,\n"
+	"\t'correlation', $11::real,\n"
+	"\t'most_common_elems', $12,\n"
+	"\t'most_common_elem_freqs', $13::real[],\n"
+	"\t'elem_count_histogram', $14::real[],\n"
+	"\t'range_length_histogram', $15,\n"
+	"\t'range_empty_frac', $16::real,\n"
+	"\t'range_bounds_histogram', $17)";
+
+/* Argument order in attribute stats import query */
+enum AttImportSqlArgs
+{
+	ATTIMPORT_SQL_VERSION = 0,
+	ATTIMPORT_SQL_SCHEMANAME,
+	ATTIMPORT_SQL_RELNAME,
+	ATTIMPORT_SQL_ATTNUM,
+	ATTIMPORT_SQL_NULL_FRAC,
+	ATTIMPORT_SQL_AVG_WIDTH,
+	ATTIMPORT_SQL_N_DISTINCT,
+	ATTIMPORT_SQL_MOST_COMMON_VALS,
+	ATTIMPORT_SQL_MOST_COMMON_FREQS,
+	ATTIMPORT_SQL_HISTOGRAM_BOUNDS,
+	ATTIMPORT_SQL_CORRELATION,
+	ATTIMPORT_SQL_MOST_COMMON_ELEMS,
+	ATTIMPORT_SQL_MOST_COMMON_ELEM_FREQS,
+	ATTIMPORT_SQL_ELEM_COUNT_HISTOGRAM,
+	ATTIMPORT_SQL_RANGE_LENGTH_HISTOGRAM,
+	ATTIMPORT_SQL_RANGE_EMPTY_FRAC,
+	ATTIMPORT_SQL_RANGE_BOUNDS_HISTOGRAM,
+	ATTIMPORT_SQL_NUM_FIELDS
+};
+
+/* Argument types in attribute stats import query */
+static const Oid attimport_argtypes[ATTIMPORT_SQL_NUM_FIELDS] =
+{
+	INT4OID, TEXTOID, TEXTOID, INT2OID,
+	TEXTOID, TEXTOID, TEXTOID, TEXTOID,
+	TEXTOID, TEXTOID, TEXTOID, TEXTOID,
+	TEXTOID, TEXTOID, TEXTOID, TEXTOID,
+	TEXTOID,
+};
+
+/*
+ * The mapping of attribute stats query columns to the positional arguments in
+ * the prepared pg_restore_attribute_stats() statement.
+ */
+typedef struct
+{
+	enum AttStatsColumns res_field;
+	enum AttImportSqlArgs arg_num;
+} AttrResultArgMap;
+
+#define NUM_MAPPED_ATTIMPORT_ARGS 13
+
+static const AttrResultArgMap attr_result_arg_map[NUM_MAPPED_ATTIMPORT_ARGS] =
+{
+	{ATTSTATS_NULL_FRAC, ATTIMPORT_SQL_NULL_FRAC},
+	{ATTSTATS_AVG_WIDTH, ATTIMPORT_SQL_AVG_WIDTH},
+	{ATTSTATS_N_DISTINCT, ATTIMPORT_SQL_N_DISTINCT},
+	{ATTSTATS_MOST_COMMON_VALS, ATTIMPORT_SQL_MOST_COMMON_VALS},
+	{ATTSTATS_MOST_COMMON_FREQS, ATTIMPORT_SQL_MOST_COMMON_FREQS},
+	{ATTSTATS_HISTOGRAM_BOUNDS, ATTIMPORT_SQL_HISTOGRAM_BOUNDS},
+	{ATTSTATS_CORRELATION, ATTIMPORT_SQL_CORRELATION},
+	{ATTSTATS_MOST_COMMON_ELEMS, ATTIMPORT_SQL_MOST_COMMON_ELEMS},
+	{ATTSTATS_MOST_COMMON_ELEM_FREQS, ATTIMPORT_SQL_MOST_COMMON_ELEM_FREQS},
+	{ATTSTATS_ELEM_COUNT_HISTOGRAM, ATTIMPORT_SQL_ELEM_COUNT_HISTOGRAM},
+	{ATTSTATS_RANGE_LENGTH_HISTOGRAM, ATTIMPORT_SQL_RANGE_LENGTH_HISTOGRAM},
+	{ATTSTATS_RANGE_EMPTY_FRAC, ATTIMPORT_SQL_RANGE_EMPTY_FRAC},
+	{ATTSTATS_RANGE_BOUNDS_HISTOGRAM, ATTIMPORT_SQL_RANGE_BOUNDS_HISTOGRAM},
+};
+
+/* Attribute stats clear query */
+static const char *attclear_sql =
+	"SELECT pg_catalog.pg_clear_attribute_stats($1, $2, $3, false)";
+
+/* Argument order in attribute stats clear query */
+enum AttClearSqlArgs
+{
+	ATTCLEAR_SQL_SCHEMANAME = 0,
+	ATTCLEAR_SQL_RELNAME,
+	ATTCLEAR_SQL_ATTNAME,
+	ATTCLEAR_SQL_NUM_FIELDS
+};
+
+/* Argument types in attribute stats clear query */
+static const Oid attclear_argtypes[ATTCLEAR_SQL_NUM_FIELDS] =
+{
+	TEXTOID, TEXTOID, TEXTOID,
+};
+
 /*
  * SQL functions
  */
@@ -403,6 +582,9 @@ static void postgresExecForeignTruncate(List *rels,
 static bool postgresAnalyzeForeignTable(Relation relation,
 										AcquireSampleRowsFunc *func,
 										BlockNumber *totalpages);
+static bool postgresImportForeignStatistics(Relation relation,
+											List *va_cols,
+											int elevel);
 static List *postgresImportForeignSchema(ImportForeignSchemaStmt *stmt,
 										 Oid serverOid);
 static void postgresGetForeignJoinPaths(PlannerInfo *root,
@@ -508,6 +690,37 @@ static int	postgresAcquireSampleRowsFunc(Relation relation, int elevel,
 										  double *totaldeadrows);
 static void analyze_row_processor(PGresult *res, int row,
 								  PgFdwAnalyzeState *astate);
+static bool fetch_remote_statistics(Relation relation,
+									List *va_cols,
+									ForeignTable *table,
+									const char *local_schemaname,
+									const char *local_relname,
+									int *p_attrcnt,
+									RemoteAttributeMapping **p_remattrmap,
+									RemoteStatsResults *remstats);
+static PGresult *fetch_relstats(PGconn *conn, Relation relation);
+static PGresult *fetch_attstats(PGconn *conn, int server_version_num,
+								const char *remote_schemaname, const char *remote_relname,
+								const char *column_list);
+static RemoteAttributeMapping *build_remattrmap(Relation relation, List *va_cols,
+												int *p_attrcnt, StringInfo column_list);
+static bool attname_in_list(const char *attname, List *va_cols);
+static int remattrmap_cmp(const void *v1, const void *v2);
+static bool match_attrmap(PGresult *res,
+						  const char *local_schemaname,
+						  const char *local_relname,
+						  const char *remote_schemaname,
+						  const char *remote_relname,
+						  int attrcnt,
+						  RemoteAttributeMapping *remattrmap);
+static bool import_fetched_statistics(const char *schemaname,
+									  const char *relname,
+									  int attrcnt,
+									  const RemoteAttributeMapping *remattrmap,
+									  RemoteStatsResults *remstats);
+static void map_field_to_arg(PGresult *res, int row, int field,
+							 int arg, Datum *values, char *nulls);
+static bool import_spi_query_ok(void);
 static void produce_tuple_asynchronously(AsyncRequest *areq, bool fetch);
 static void fetch_more_data_begin(AsyncRequest *areq);
 static void complete_pending_request(AsyncRequest *areq);
@@ -596,6 +809,7 @@ postgres_fdw_handler(PG_FUNCTION_ARGS)
 
 	/* Support functions for ANALYZE */
 	routine->AnalyzeForeignTable = postgresAnalyzeForeignTable;
+	routine->ImportForeignStatistics = postgresImportForeignStatistics;
 
 	/* Support functions for IMPORT FOREIGN SCHEMA */
 	routine->ImportForeignSchema = postgresImportForeignSchema;
@@ -4975,10 +5189,11 @@ postgresGetAnalyzeInfoForForeignTable(Relation relation, bool *can_tablesample)
 	if (PQresultStatus(res) != PGRES_TUPLES_OK)
 		pgfdw_report_error(res, conn, sql.data);
 
-	if (PQntuples(res) != 1 || PQnfields(res) != 2)
+	if (PQntuples(res) != 1 || PQnfields(res) != RELSTATS_NUM_FIELDS)
 		elog(ERROR, "unexpected result from deparseAnalyzeInfoSql query");
-	reltuples = strtod(PQgetvalue(res, 0, 0), NULL);
-	relkind = *(PQgetvalue(res, 0, 1));
+	/* We don't use relpages here */
+	reltuples = strtod(PQgetvalue(res, 0, RELSTATS_RELTUPLES), NULL);
+	relkind = *(PQgetvalue(res, 0, RELSTATS_RELKIND));
 	PQclear(res);
 
 	ReleaseConnection(conn);
@@ -5367,6 +5582,719 @@ analyze_row_processor(PGresult *res, int row, PgFdwAnalyzeState *astate)
 	}
 }
 
+/*
+ * postgresImportForeignStatistics
+ * 		Attempt to fetch/restore remote statistics instead of sampling.
+ */
+static bool
+postgresImportForeignStatistics(Relation relation, List *va_cols, int elevel)
+{
+	const char *schemaname = NULL;
+	const char *relname = NULL;
+	ForeignTable *table;
+	ForeignServer *server;
+	RemoteStatsResults remstats = {.rel = NULL, .att = NULL};
+	RemoteAttributeMapping *remattrmap = NULL;
+	int			attrcnt = 0;
+	bool		restore_stats = false;
+	bool		ok = false;
+	ListCell   *lc;
+
+	table = GetForeignTable(RelationGetRelid(relation));
+	server = GetForeignServer(table->serverid);
+	schemaname = get_namespace_name(RelationGetNamespace(relation));
+	relname = RelationGetRelationName(relation);
+
+	/*
+	 * Check whether the restore_stats option is enabled on the foreign table.
+	 * If not, silently ignore the foreign table.
+	 *
+	 * Server-level options can be overridden by table-level options, so check
+	 * server-level first.
+	 */
+	foreach(lc, server->options)
+	{
+		DefElem    *def = (DefElem *) lfirst(lc);
+
+		if (strcmp(def->defname, "restore_stats") == 0)
+		{
+			restore_stats = defGetBoolean(def);
+			break;
+		}
+	}
+	foreach(lc, table->options)
+	{
+		DefElem    *def = (DefElem *) lfirst(lc);
+
+		if (strcmp(def->defname, "restore_stats") == 0)
+		{
+			restore_stats = defGetBoolean(def);
+			break;
+		}
+	}
+	if (!restore_stats)
+		return false;
+
+	/*
+	 * We don't currently support statistics import for foreign tables with
+	 * extended statistics objects.
+	 */
+	if (HasRelationExtStatistics(relation))
+	{
+		ereport(WARNING,
+				errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				errmsg("cannot import statistics for foreign table \"%s.%s\" --- this foreign table has extended statistics objects",
+					   schemaname, relname));
+		return false;
+	}
+
+	/*
+	 * OK, let's do it.
+	 */
+	ereport(elevel,
+			(errmsg("importing statistics for foreign table \"%s.%s\"",
+					schemaname, relname)));
+
+	ok = fetch_remote_statistics(relation, va_cols,
+								 table, schemaname, relname,
+								 &attrcnt, &remattrmap, &remstats);
+
+	if (ok)
+		ok = import_fetched_statistics(schemaname, relname,
+									   attrcnt, remattrmap, &remstats);
+
+	if (ok)
+		ereport(elevel,
+				(errmsg("finished importing statistics for foreign table \"%s.%s\"",
+						schemaname, relname)));
+
+	PQclear(remstats.rel);
+	PQclear(remstats.att);
+	if (remattrmap)
+		pfree(remattrmap);
+
+	return ok;
+}
+
+/*
+ * Attempt to fetch statistics from a remote server.
+ */
+static bool
+fetch_remote_statistics(Relation relation,
+						List *va_cols,
+						ForeignTable *table,
+						const char *local_schemaname,
+						const char *local_relname,
+						int *p_attrcnt,
+						RemoteAttributeMapping **p_remattrmap,
+						RemoteStatsResults *remstats)
+{
+	const char *remote_schemaname = NULL;
+	const char *remote_relname = NULL;
+	UserMapping *user;
+	PGconn	   *conn;
+	PGresult   *relstats = NULL;
+	PGresult   *attstats = NULL;
+	int			server_version_num;
+	RemoteAttributeMapping *remattrmap = NULL;
+	int			attrcnt = 0;
+	char		relkind;
+	double		reltuples;
+	bool		ok = false;
+	ListCell   *lc;
+
+	/*
+	 * Assume the remote schema/relation names are the same as the local name
+	 * unless the foreign table's options tell us otherwise.
+	 */
+	remote_schemaname = local_schemaname;
+	remote_relname = local_relname;
+	foreach(lc, table->options)
+	{
+		DefElem    *def = (DefElem *) lfirst(lc);
+
+		if (strcmp(def->defname, "schema_name") == 0)
+			remote_schemaname = defGetString(def);
+		else if (strcmp(def->defname, "table_name") == 0)
+			remote_relname = defGetString(def);
+	}
+
+	/*
+	 * Get connection to the foreign server.  Connection manager will
+	 * establish new connection if necessary.
+	 */
+	user = GetUserMapping(GetUserId(), table->serverid);
+	conn = GetConnection(user, false, NULL);
+	remstats->server_version_num = server_version_num = PQserverVersion(conn);
+
+	/* Fetch relation stats. */
+	remstats->rel = relstats = fetch_relstats(conn, relation);
+
+	/*
+	 * Verify that the remote table is the sort that can have meaningful stats
+	 * in pg_stats.
+	 *
+	 * Note that while relations of kinds RELKIND_INDEX and
+	 * RELKIND_PARTITIONED_INDEX can have rows in pg_stats, they obviously
+	 * can't support a foreign table.
+	 */
+	relkind = *PQgetvalue(relstats, 0, RELSTATS_RELKIND);
+	switch (relkind)
+	{
+		case RELKIND_RELATION:
+		case RELKIND_FOREIGN_TABLE:
+		case RELKIND_MATVIEW:
+		case RELKIND_PARTITIONED_TABLE:
+			break;
+		default:
+			ereport(WARNING,
+					errmsg("could not import statistics for foreign table \"%s.%s\" --- remote table \"%s.%s\" is of relkind \"%c\" which cannot have statistics",
+						   local_schemaname, local_relname,
+						   remote_schemaname, remote_relname, relkind));
+			goto fetch_cleanup;
+	}
+
+	/*
+	 * If the reltuples value > 0, then then we can expect to find attribute
+	 * stats for the remote table.
+	 *
+	 * In v14 or latter, if a reltuples value is -1, it means the table has
+	 * never been analyzed, so we wouldn't expect to find the stats for the
+	 * table; fallback to sampling in that case.  If the value is 0, it means
+	 * it was empty; in which case skip the stats and import relation stats
+	 * only.
+	 *
+	 * In versions prior to v14, a value of 0 was ambiguous; it could mean
+	 * that the table had never been analyzed, or that it was empty.  Either
+	 * way, we wouldn't expect to find the stats for the table, so we fallback
+	 * to sampling.
+	 */
+	reltuples = strtod(PQgetvalue(relstats, 0, RELSTATS_RELTUPLES), NULL);
+	if (((server_version_num < 140000) && (reltuples == 0)) ||
+		((server_version_num >= 140000) && (reltuples == -1)))
+	{
+		ereport(WARNING,
+				errmsg("could not import statistics for foreign table \"%s.%s\" --- remote table \"%s.%s\" has no relation statistics to import",
+					   local_schemaname, local_relname,
+					   remote_schemaname, remote_relname));
+		goto fetch_cleanup;
+	}
+
+
+	if (reltuples > 0)
+	{
+		StringInfoData column_list;
+
+		*p_remattrmap = remattrmap = build_remattrmap(relation, va_cols,
+													  &attrcnt, &column_list);
+		*p_attrcnt = attrcnt;
+
+		if (attrcnt > 0)
+		{
+			/* Fetch attribute stats. */
+			remstats->att = attstats = fetch_attstats(conn,
+													  server_version_num,
+													  remote_schemaname,
+													  remote_relname,
+													  column_list.data);
+
+			/* If any attribute statsare missing, fallback to sampling. */
+			if (!match_attrmap(attstats,
+							   local_schemaname, local_relname,
+							   remote_schemaname, remote_relname,
+							   attrcnt, remattrmap))
+				goto fetch_cleanup;
+		}
+	}
+
+	ok = true;
+
+ fetch_cleanup:
+	ReleaseConnection(conn);
+	return ok;
+}
+
+/*
+ * Attempt to fetch remote relation stats.
+ */
+static PGresult *
+fetch_relstats(PGconn *conn, Relation relation)
+{
+	StringInfoData sql;
+	PGresult   *res;
+
+	initStringInfo(&sql);
+	deparseAnalyzeInfoSql(&sql, relation);
+
+	res = pgfdw_exec_query(conn, sql.data, NULL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pgfdw_report_error(res, conn, sql.data);
+
+	if (PQntuples(res) != 1 || PQnfields(res) != RELSTATS_NUM_FIELDS)
+		elog(ERROR, "unexpected result from deparseAnalyzeInfoSql query");
+
+	return res;
+}
+
+/*
+ * Attempt to fetch remote attribute stats.
+ */
+static PGresult *
+fetch_attstats(PGconn *conn, int server_version_num,
+			   const char *remote_schemaname, const char *remote_relname,
+			   const char *column_list)
+{
+	StringInfoData sql;
+	PGresult   *res;
+
+	initStringInfo(&sql);
+	appendStringInfoString(&sql,
+						   "SELECT DISTINCT ON (attname COLLATE \"C\") attname,"
+						   " null_frac,"
+						   " avg_width,"
+						   " n_distinct,"
+						   " most_common_vals,"
+						   " most_common_freqs,"
+						   " histogram_bounds,"
+						   " correlation,");
+
+	/* Elements stats are supported since Postgres 9.2 */
+	if (server_version_num >= 92000)
+		appendStringInfoString(&sql,
+							   " most_common_elems,"
+							   " most_common_elem_freqs,"
+							   " elem_count_histogram,");
+	else
+		appendStringInfoString(&sql,
+							   " NULL, NULL, NULL,");
+
+	/* Range stats are supported since Postgres 17 */
+	if (server_version_num >= 170000)
+		appendStringInfoString(&sql,
+							   " range_length_histogram,"
+							   " range_empty_frac,"
+							   " range_bounds_histogram");
+	else
+		appendStringInfoString(&sql,
+							   " NULL, NULL, NULL,");
+
+	appendStringInfoString(&sql,
+						   " FROM pg_catalog.pg_stats"
+						   " WHERE schemaname = ");
+	deparseStringLiteral(&sql, remote_schemaname);
+	appendStringInfoString(&sql,
+						   " AND tablename = ");
+	deparseStringLiteral(&sql, remote_relname);
+	appendStringInfo(&sql,
+					 " AND attname = ANY('%s'::text[])",
+					 column_list);
+
+	/* inherited is supported since Postgres 9.0 */
+	if (server_version_num >= 90000)
+		appendStringInfoString(&sql,
+							   " ORDER BY attname COLLATE \"C\", inherited DESC");
+	else
+		appendStringInfoString(&sql,
+							   " ORDER BY attname COLLATE \"C\"");
+
+	res = pgfdw_exec_query(conn, sql.data, NULL);
+	if (PQresultStatus(res) != PGRES_TUPLES_OK)
+		pgfdw_report_error(res, conn, sql.data);
+
+	if (PQnfields(res) != ATTSTATS_NUM_FIELDS)
+		elog(ERROR, "unexpected result from fetch_attstats query");
+
+	return res;
+}
+
+/*
+ * Build the mapping of local columns to remote columns and create a column
+ * list used for constructing the fetch_attstats query.
+ */
+static RemoteAttributeMapping *
+build_remattrmap(Relation relation, List *va_cols,
+				 int *p_attrcnt, StringInfo column_list)
+{
+	TupleDesc	tupdesc = RelationGetDescr(relation);
+	RemoteAttributeMapping *remattrmap = NULL;
+	int			attrcnt = 0;
+
+	remattrmap = palloc_array(RemoteAttributeMapping, tupdesc->natts);
+	initStringInfo(column_list);
+	appendStringInfoChar(column_list, '{');
+	for (int i = 0; i < tupdesc->natts; i++)
+	{
+		Form_pg_attribute attr = TupleDescAttr(tupdesc, i);
+		char	   *attname = NameStr(attr->attname);
+		AttrNumber	attnum = attr->attnum;
+		char	   *remote_attname;
+		List	   *fc_options;
+		ListCell   *lc;
+
+		/* If a list is specified, exclude any attnames not in it. */
+		if (!attname_in_list(attname, va_cols))
+			continue;
+
+		if (!attribute_is_analyzable(relation, attnum, attr, NULL))
+			continue;
+
+		/* If the column_name option is not specified, go with attname. */
+		remote_attname = attname;
+		fc_options = GetForeignColumnOptions(RelationGetRelid(relation), attnum);
+		foreach(lc, fc_options)
+		{
+			DefElem    *def = (DefElem *) lfirst(lc);
+
+			if (strcmp(def->defname, "column_name") == 0)
+			{
+				remote_attname = defGetString(def);
+				break;
+			}
+		}
+
+		if (attrcnt > 0)
+			appendStringInfoString(column_list, ", ");
+		appendStringInfoString(column_list, quote_identifier(remote_attname));
+
+		remattrmap[attrcnt].local_attnum = attnum;
+		strncpy(remattrmap[attrcnt].local_attname, attname, NAMEDATALEN);
+		strncpy(remattrmap[attrcnt].remote_attname, remote_attname, NAMEDATALEN);
+		remattrmap[attrcnt].res_index = -1;
+		attrcnt++;
+	}
+	appendStringInfoChar(column_list, '}');
+
+	/* Sort mapping by remote attribute name if needed. */
+	if (attrcnt > 1)
+		qsort(remattrmap, attrcnt, sizeof(RemoteAttributeMapping), remattrmap_cmp);
+
+	*p_attrcnt = attrcnt;
+	return remattrmap;
+}
+
+/*
+ * Test if an attribute name is in the list.
+ *
+ * An empty list means that all attribute names are in the list.
+ */
+static bool
+attname_in_list(const char *attname, List *va_cols)
+{
+	ListCell   *le;
+
+	if (va_cols == NIL)
+		return true;
+
+	foreach(le, va_cols)
+	{
+		char	   *col = strVal(lfirst(le));
+
+		if (strcmp(attname, col) == 0)
+			return true;
+	}
+	return false;
+}
+
+/*
+ * Compare two RemoteAttributeMappings for sorting.
+ */
+static int
+remattrmap_cmp(const void *v1, const void *v2)
+{
+	const		RemoteAttributeMapping *r1 = v1;
+	const		RemoteAttributeMapping *r2 = v2;
+
+	return strncmp(r1->remote_attname, r2->remote_attname, NAMEDATALEN);
+}
+
+/*
+ * Match local columns to result set rows.
+ *
+ * As the result set consists of the attribute stats for some/all of distinct
+ * mapped remote columns in the RemoteAttributeMapping, every entry in it
+ * should have at most one match in the result set; which is also ordered by
+ * attname, so we find such pairs by doing a merge join.
+ *
+ * Returns true if every entry in it has a match, and false if not.
+ */
+static bool
+match_attrmap(PGresult *res,
+			  const char *local_schemaname,
+			  const char *local_relname,
+			  const char *remote_schemaname,
+			  const char *remote_relname,
+			  int attrcnt,
+			  RemoteAttributeMapping *remattrmap)
+{
+	int			numrows = PQntuples(res);
+	int			row = -1;
+
+	/* No work if there are no stats rows. */
+	if (numrows == 0)
+	{
+		ereport(WARNING,
+				errmsg("could not import statistics for foreign table \"%s.%s\" --- remote table \"%s.%s\" has no attribute statistics to import",
+					   local_schemaname, local_relname,
+					   remote_schemaname, remote_relname));
+		return false;
+	}
+
+	/* Scan all entries in the RemoteAttributeMapping. */
+	for (int mapidx = 0; mapidx < attrcnt; mapidx++)
+	{
+		/*
+		 * First, check whether the entry matches the current stats row, if it
+		 * is set.
+		 */
+		if (row >= 0 &&
+			strcmp(remattrmap[mapidx].remote_attname,
+				   PQgetvalue(res, row, ATTSTATS_ATTNAME)) == 0)
+		{
+			remattrmap[mapidx].res_index = row;
+			continue;
+		}
+
+		/*
+		 * If we've exhausted all stats rows, it means the stats for the entry
+		 * are missing.
+		 */
+		if (row >= numrows - 1)
+		{
+			ereport(WARNING,
+					errmsg("could not import statistics for foreign table \"%s.%s\" --- no attribute statistics found for column \"%s\" of remote table \"%s.%s\"",
+						   local_schemaname, local_relname,
+						   remattrmap[mapidx].remote_attname,
+						   remote_schemaname, remote_relname));
+			return false;
+		}
+
+		/* Advance to the next stats row. */
+		row += 1;
+
+		/*
+		 * If the attname in the entry is less than that in the next stats row,
+		 * it means the stats for the entry are missing.
+		 */
+		if (strcmp(remattrmap[mapidx].remote_attname,
+				   PQgetvalue(res, row, ATTSTATS_ATTNAME)) < 0)
+		{
+			ereport(WARNING,
+					errmsg("could not import statistics for foreign table \"%s.%s\" --- no attribute statistics found for column \"%s\" of remote table \"%s.%s\"",
+						   local_schemaname, local_relname,
+						   remattrmap[mapidx].remote_attname,
+						   remote_schemaname, remote_relname));
+			return false;
+		}
+
+		/* We should not have got a stats row we didn't expect. */
+		if (strcmp(remattrmap[mapidx].remote_attname,
+				   PQgetvalue(res, row, ATTSTATS_ATTNAME)) > 0)
+			elog(ERROR, "unexpected result from fetch_attstats query");
+
+		/* We found a match. */
+		Assert(strcmp(remattrmap[mapidx].remote_attname,
+					  PQgetvalue(res, row, ATTSTATS_ATTNAME)) == 0);
+		remattrmap[mapidx].res_index = row;
+	}
+
+	/* We should have exhausted all stats rows. */
+	if (row < numrows - 1)
+		elog(ERROR, "unexpected result from fetch_attstats query");
+
+	return true;
+}
+
+/*
+ * Import fetched statistics into the local statistics tables.
+ */
+static bool
+import_fetched_statistics(const char *schemaname,
+						  const char *relname,
+						  int attrcnt,
+						  const RemoteAttributeMapping *remattrmap,
+						  RemoteStatsResults *remstats)
+{
+	SPIPlanPtr	attimport_plan = NULL;
+	SPIPlanPtr	attclear_plan = NULL;
+	Datum		values[ATTIMPORT_SQL_NUM_FIELDS];
+	char		nulls[ATTIMPORT_SQL_NUM_FIELDS];
+	int			spirc;
+	bool		ok = false;
+
+	/* Assign all the invariant parameters common to relation/attribute stats */
+	values[ATTIMPORT_SQL_VERSION] = Int32GetDatum(remstats->server_version_num);
+	nulls[ATTIMPORT_SQL_VERSION] = ' ';
+
+	values[ATTIMPORT_SQL_SCHEMANAME] = CStringGetTextDatum(schemaname);
+	nulls[ATTIMPORT_SQL_SCHEMANAME] = ' ';
+
+	values[ATTIMPORT_SQL_RELNAME] = CStringGetTextDatum(relname);
+	nulls[ATTIMPORT_SQL_RELNAME] = ' ';
+
+	SPI_connect();
+
+	/*
+	 * We import attribute statistics first, if any, because those are more
+	 * prone to errors.  This avoids making a modification of pg_class that
+	 * will just get rolled back by a failed attribute import.
+	 */
+	if (remstats->att != NULL)
+	{
+		Assert(PQnfields(remstats->att) == ATTSTATS_NUM_FIELDS);
+		Assert(PQntuples(remstats->att) >= 1);
+
+		attimport_plan = SPI_prepare(attimport_sql, ATTIMPORT_SQL_NUM_FIELDS,
+									 (Oid *) attimport_argtypes);
+		if (attimport_plan == NULL)
+			elog(ERROR, "failed to prepare attimport_sql query");
+
+		attclear_plan = SPI_prepare(attclear_sql, ATTCLEAR_SQL_NUM_FIELDS,
+									(Oid *) attclear_argtypes);
+		if (attclear_plan == NULL)
+			elog(ERROR, "failed to prepare attclear_sql query");
+
+		nulls[ATTIMPORT_SQL_ATTNUM] = ' ';
+
+		for (int mapidx = 0; mapidx < attrcnt; mapidx++)
+		{
+			int			row = remattrmap[mapidx].res_index;
+			Datum		*values2 = values + 1;
+			char		*nulls2 = nulls + 1;
+
+			/* All mappings should have been assigned a result set row. */
+			Assert(row >= 0);
+
+			/*
+			 * Check for user-requested abort.
+			 */
+			CHECK_FOR_INTERRUPTS();
+
+			/*
+			 * First, clear existing attribute stats.
+			 *
+			 * We can re-use the values/nulls because the number of parameters
+			 * is less and the first two params are the same as the second and
+			 * third ones in attimport_sql.
+			 */
+			values2[ATTCLEAR_SQL_ATTNAME] =
+				CStringGetTextDatum(remattrmap[mapidx].local_attname);
+
+			spirc = SPI_execute_plan(attclear_plan, values2, nulls2, false, 1);
+			if (spirc != SPI_OK_SELECT)
+				elog(ERROR, "failed to execute attclear_sql query for column \"%s\" of foreign table \"%s.%s\"",
+					 remattrmap[mapidx].local_attname, schemaname, relname);
+
+			values[ATTIMPORT_SQL_ATTNUM] =
+				Int16GetDatum(remattrmap[mapidx].local_attnum);
+
+			/* Loop through all mappable columns to set remaining arguments */
+			for (int i = 0; i < NUM_MAPPED_ATTIMPORT_ARGS; i++)
+				map_field_to_arg(remstats->att, row,
+								 attr_result_arg_map[i].res_field,
+								 attr_result_arg_map[i].arg_num,
+								 values, nulls);
+
+			spirc = SPI_execute_plan(attimport_plan, values, nulls, false, 1);
+			if (spirc != SPI_OK_SELECT)
+				elog(ERROR, "failed to execute attimport_sql query for column \"%s\" of foreign table \"%s.%s\"",
+					 remattrmap[mapidx].local_attname, schemaname, relname);
+
+			if (!import_spi_query_ok())
+			{
+				ereport(WARNING,
+						errmsg("could not import statistics for foreign table \"%s.%s\" --- attribute statistics import failed for column \"%s\" of this foreign table",
+							   schemaname, relname,
+							   remattrmap[mapidx].local_attname));
+				goto import_cleanup;
+			}
+		}
+	}
+
+	/*
+	 * Import relation stats.  We only perform this once, so there is no point
+	 * in preparing the statement.
+	 *
+	 * We can re-use the values/nulls because the number of parameters is less
+	 * and the first three params are the same as attimport_sql.
+	 */
+	Assert(remstats->rel != NULL);
+	Assert(PQnfields(remstats->rel) == RELSTATS_NUM_FIELDS);
+	Assert(PQntuples(remstats->rel) == 1);
+	map_field_to_arg(remstats->rel, 0, RELSTATS_RELPAGES,
+					 RELIMPORT_SQL_RELPAGES, values, nulls);
+	map_field_to_arg(remstats->rel, 0, RELSTATS_RELTUPLES,
+					 RELIMPORT_SQL_RELTUPLES, values, nulls);
+
+	spirc = SPI_execute_with_args(relimport_sql,
+								  RELIMPORT_SQL_NUM_FIELDS,
+								  (Oid *) relimport_argtypes,
+								  values, nulls, false, 1);
+	if (spirc != SPI_OK_SELECT)
+		elog(ERROR, "failed to execute relimport_sql query for foreign table \"%s.%s\"",
+			 schemaname, relname);
+
+	if (!import_spi_query_ok())
+	{
+		ereport(WARNING,
+				errmsg("could not import statistics for foreign table \"%s.%s\" --- relation statistics import failed for this foreign table",
+					   schemaname, relname));
+		goto import_cleanup;
+	}
+
+	ok = true;
+
+import_cleanup:
+	if (attimport_plan)
+		SPI_freeplan(attimport_plan);
+	if (attclear_plan)
+		SPI_freeplan(attclear_plan);
+	SPI_finish();
+	return ok;
+}
+
+/*
+ * Move a string value from a result set to a Text value of a Datum array.
+ */
+static void
+map_field_to_arg(PGresult *res, int row, int field,
+				 int arg, Datum *values, char *nulls)
+{
+	if (PQgetisnull(res, row, field))
+	{
+		values[arg] = (Datum) 0;
+		nulls[arg] = 'n';
+	}
+	else
+	{
+		const char *s = PQgetvalue(res, row, field);
+
+		values[arg] = CStringGetTextDatum(s);
+		nulls[arg] = ' ';
+	}
+}
+
+/*
+ * Check the 1x1 result set of a pg_restore_*_stats() command for success.
+ */
+static bool
+import_spi_query_ok(void)
+{
+	TupleDesc	tupdesc;
+	Datum		dat;
+	bool		isnull;
+
+	Assert(SPI_tuptable != NULL);
+	Assert(SPI_processed == 1);
+
+	tupdesc = SPI_tuptable->tupdesc;
+	Assert(tupdesc->natts == 1);
+	Assert(TupleDescAttr(tupdesc, 0)->atttypid == BOOLOID);
+	dat = SPI_getbinval(SPI_tuptable->vals[0], tupdesc, 1, &isnull);
+	Assert(!isnull);
+
+	return DatumGetBool(dat);
+}
+
 /*
  * Import a foreign schema
  */
diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql
index 59963e298b8..79ad5be8bf9 100644
--- a/contrib/postgres_fdw/sql/postgres_fdw.sql
+++ b/contrib/postgres_fdw/sql/postgres_fdw.sql
@@ -4515,6 +4515,70 @@ ANALYZE analyze_ftable;
 DROP FOREIGN TABLE analyze_ftable;
 DROP TABLE analyze_table;
 
+-- ===================================================================
+-- test for statistics import
+-- ===================================================================
+
+CREATE TABLE simport_table (c1 int, c2 text);
+CREATE FOREIGN TABLE simport_ftable (c1 int, c2 text, cx int)
+       SERVER loopback OPTIONS (table_name 'simport_table');
+ALTER FOREIGN TABLE simport_ftable ALTER COLUMN cx OPTIONS (ADD column_name 'c1');
+ALTER FOREIGN TABLE simport_ftable OPTIONS (ADD restore_stats 'true');
+
+ANALYZE simport_ftable;                   -- should fail
+
+ANALYZE simport_table;
+
+ANALYZE VERBOSE simport_ftable;           -- should work
+
+ALTER TABLE simport_table ALTER COLUMN c1 SET STATISTICS 0;
+ALTER TABLE simport_table ALTER COLUMN c2 SET STATISTICS 0;
+INSERT INTO simport_table VALUES (1, 'foo'), (1, 'foo'), (2, 'bar'), (2, 'bar');
+ANALYZE simport_table;
+
+ANALYZE simport_ftable;                   -- should fail
+
+ALTER TABLE simport_table ALTER COLUMN c1 SET STATISTICS DEFAULT;
+ANALYZE simport_table;
+
+ANALYZE simport_ftable;                   -- should fail
+
+ALTER TABLE simport_table ALTER COLUMN c2 SET STATISTICS DEFAULT;
+ANALYZE simport_table;
+
+ANALYZE VERBOSE simport_ftable;           -- should work
+
+ANALYZE VERBOSE simport_ftable (c1);      -- should work
+
+ANALYZE VERBOSE simport_ftable (c2);      -- should work
+
+ANALYZE VERBOSE simport_ftable (c1, cx);  -- should work
+
+ANALYZE VERBOSE simport_ftable (c2, cx);  -- should work
+
+CREATE STATISTICS stats (dependencies) ON c1, c2 FROM simport_ftable;
+
+ANALYZE simport_ftable;                   -- should fail
+
+DROP STATISTICS stats;
+
+ANALYZE simport_ftable (cid);             -- should fail
+
+ANALYZE simport_ftable (c1, c1);          -- should fail
+
+CREATE VIEW simport_view AS SELECT * FROM simport_table;
+CREATE FOREIGN TABLE simport_fview (c1 int, c2 text)
+       SERVER loopback OPTIONS (table_name 'simport_view');
+ALTER FOREIGN TABLE simport_fview OPTIONS (ADD restore_stats 'true');
+
+ANALYZE simport_fview;                    -- should fail
+
+-- cleanup
+DROP FOREIGN TABLE simport_ftable;
+DROP FOREIGN TABLE simport_fview;
+DROP VIEW simport_view;
+DROP TABLE simport_table;
+
 -- ===================================================================
 -- test for postgres_fdw_get_connections function with check_conn = true
 -- ===================================================================
diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml
index f601d22d191..a80067d2ee3 100644
--- a/doc/src/sgml/fdwhandler.sgml
+++ b/doc/src/sgml/fdwhandler.sgml
@@ -1409,6 +1409,45 @@ AcquireSampleRowsFunc(Relation relation,
      if the FDW does not have any concept of dead rows.)
     </para>
 
+    <para>
+<programlisting>
+bool
+ImportForeignStatistics(Relation relation,
+                        List *va_cols,
+                        int elevel);
+</programlisting>
+
+     This function is called before the <function>AnalyzeForeignTable</function>
+     callback function when <xref linkend="sql-analyze"/> is executed on a
+     foreign table, and is used to import remotely-calculated statistics (both
+     table-level and column-level) for the foreign table directly to the local
+     server.
+     <literal>relation</literal> is the <structname>Relation</structname> struct
+     describing the target foreign table.  <literal>va_cols</literal>, if not
+     NIL, contains the columns specified in the <command>ANALYZE</command>
+     command.  <literal>elevel</literal> contains a flag indicating a logging
+     level to use.
+     If the FDW imports statistics successfully, it should return
+     <literal>true</literal>.  Otherwise, return <literal>false</literal>, in
+     which case <function>AnalyzeForeignTable</function> callback function is
+     called to collect statistics locally, if supported.
+    </para>
+
+    <para>
+     For reference, the logic for calculating statistics in
+     <productname>PostgreSQL</productname> are found in
+     <filename>src/backend/command/analyze.c</filename>.
+     It's recommended to import table-level and column-level statistics for the
+     foreign table using <function>pg_restore_relation_stats</function> and
+     <function>pg_restore_attribute_stats</function>, respectively.
+    </para>
+
+    <para>
+     If the FDW does not support importing remote statistics for any tables, the
+     <function>ImportForeignStatistics</function> pointer can be set to
+     <literal>NULL</literal>.
+    </para>
+
    </sect2>
 
    <sect2 id="fdw-callbacks-import">
diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml
index 9185c76f932..7ac760c3174 100644
--- a/doc/src/sgml/postgres-fdw.sgml
+++ b/doc/src/sgml/postgres-fdw.sgml
@@ -332,7 +332,7 @@ OPTIONS (ADD password_required 'false');
    </para>
 
    <para>
-    The following option controls how such an <command>ANALYZE</command>
+    The following options control how such an <command>ANALYZE</command>
     operation behaves:
    </para>
 
@@ -364,6 +364,33 @@ OPTIONS (ADD password_required 'false');
      </listitem>
     </varlistentry>
 
+    <varlistentry>
+     <term><literal>restore_stats</literal> (<type>boolean</type>)</term>
+     <listitem>
+      <para>
+       This option, which can be specified for a foreign table or a foreign
+       server, determines if <command>ANALYZE</command> on a foreign table
+       will instead attempt to fetch the existing statistics for the foreign
+       table on the remote server, and restore those statistics directly to
+       the local server.  If the attempt failed, statistics are collected by
+       row sampling on the foreign table.
+       This option is only useful if the remote table is one that can have
+       regular statistics (tables and materialized views).
+       When using this option, <emphasis>it is the user's responsibility
+       </emphasis> to ensure that the existing statistics for the foreign
+       table are up-to-date.
+       The default is <literal>false</literal>.
+      </para>
+
+      <para>
+       If the foreign table is a partition of a partitioned table, analyzing
+       the partitioned table will still result in row sampling on the foreign
+       table regardless of this setting, though direct analysis of the foreign
+       table would have attempted to restore remote statistics first.
+      </para>
+     </listitem>
+    </varlistentry>
+
    </variablelist>
 
   </sect3>
diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c
index 49a5cdf579c..3331833a363 100644
--- a/src/backend/commands/analyze.c
+++ b/src/backend/commands/analyze.c
@@ -83,6 +83,7 @@ static void compute_index_stats(Relation onerel, double totalrows,
 								AnlIndexData *indexdata, int nindexes,
 								HeapTuple *rows, int numrows,
 								MemoryContext col_context);
+static void validate_va_cols_list(Relation onerel, List *va_cols);
 static VacAttrStats *examine_attribute(Relation onerel, int attnum,
 									   Node *index_expr);
 static int	acquire_sample_rows(Relation onerel, int elevel,
@@ -114,6 +115,7 @@ analyze_rel(Oid relid, RangeVar *relation,
 	int			elevel;
 	AcquireSampleRowsFunc acquirefunc = NULL;
 	BlockNumber relpages = 0;
+	bool		stats_imported = false;
 
 	/* Select logging level */
 	if (params->options & VACOPT_VERBOSE)
@@ -182,6 +184,27 @@ analyze_rel(Oid relid, RangeVar *relation,
 		return;
 	}
 
+	/*
+	 * Check the given list of columns
+	 */
+	if (va_cols != NIL)
+		validate_va_cols_list(onerel, va_cols);
+
+	/*
+	 * Initialize progress reporting before setup for regular/foreign tables.
+	 * (For the former, the time spent on it would be negligible, but for the
+	 * latter, if FDWs support statistics import or analysis, they'd do some
+	 * work that needs the remote access, so the time might be non-negligible.)
+	 */
+	pgstat_progress_start_command(PROGRESS_COMMAND_ANALYZE,
+								  RelationGetRelid(onerel));
+	if (AmAutoVacuumWorkerProcess())
+		pgstat_progress_update_param(PROGRESS_ANALYZE_STARTED_BY,
+									 PROGRESS_ANALYZE_STARTED_BY_AUTOVACUUM);
+	else
+		pgstat_progress_update_param(PROGRESS_ANALYZE_STARTED_BY,
+									 PROGRESS_ANALYZE_STARTED_BY_MANUAL);
+
 	/*
 	 * Check that it's of an analyzable relkind, and set up appropriately.
 	 */
@@ -196,26 +219,33 @@ analyze_rel(Oid relid, RangeVar *relation,
 	else if (onerel->rd_rel->relkind == RELKIND_FOREIGN_TABLE)
 	{
 		/*
-		 * For a foreign table, call the FDW's hook function to see whether it
-		 * supports analysis.
+		 * For a foreign table, call the FDW's hook functions to see whether
+		 * it supports statistics import or analysis.
 		 */
 		FdwRoutine *fdwroutine;
-		bool		ok = false;
 
 		fdwroutine = GetFdwRoutineForRelation(onerel, false);
 
-		if (fdwroutine->AnalyzeForeignTable != NULL)
-			ok = fdwroutine->AnalyzeForeignTable(onerel,
-												 &acquirefunc,
-												 &relpages);
-
-		if (!ok)
+		if (fdwroutine->ImportForeignStatistics != NULL &&
+			fdwroutine->ImportForeignStatistics(onerel, va_cols, elevel))
+			stats_imported = true;
+		else
 		{
-			ereport(WARNING,
-					(errmsg("skipping \"%s\" --- cannot analyze this foreign table",
-							RelationGetRelationName(onerel))));
-			relation_close(onerel, ShareUpdateExclusiveLock);
-			return;
+			bool		ok = false;
+
+			if (fdwroutine->AnalyzeForeignTable != NULL)
+				ok = fdwroutine->AnalyzeForeignTable(onerel,
+													 &acquirefunc,
+													 &relpages);
+
+			if (!ok)
+			{
+				ereport(WARNING,
+						errmsg("skipping \"%s\" -- cannot analyze this foreign table.",
+							   RelationGetRelationName(onerel)));
+				relation_close(onerel, ShareUpdateExclusiveLock);
+				goto out;
+			}
 		}
 	}
 	else if (onerel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE)
@@ -232,26 +262,16 @@ analyze_rel(Oid relid, RangeVar *relation,
 					(errmsg("skipping \"%s\" --- cannot analyze non-tables or special system tables",
 							RelationGetRelationName(onerel))));
 		relation_close(onerel, ShareUpdateExclusiveLock);
-		return;
+		goto out;
 	}
 
-	/*
-	 * OK, let's do it.  First, initialize progress reporting.
-	 */
-	pgstat_progress_start_command(PROGRESS_COMMAND_ANALYZE,
-								  RelationGetRelid(onerel));
-	if (AmAutoVacuumWorkerProcess())
-		pgstat_progress_update_param(PROGRESS_ANALYZE_STARTED_BY,
-									 PROGRESS_ANALYZE_STARTED_BY_AUTOVACUUM);
-	else
-		pgstat_progress_update_param(PROGRESS_ANALYZE_STARTED_BY,
-									 PROGRESS_ANALYZE_STARTED_BY_MANUAL);
-
 	/*
 	 * Do the normal non-recursive ANALYZE.  We can skip this for partitioned
-	 * tables, which don't contain any rows.
+	 * tables, which don't contain any rows, and foreign tables that
+	 * successfully imported statistics.
 	 */
-	if (onerel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+	if ((onerel->rd_rel->relkind != RELKIND_PARTITIONED_TABLE)
+		&& !stats_imported)
 		do_analyze_rel(onerel, params, va_cols, acquirefunc,
 					   relpages, false, in_outer_xact, elevel);
 
@@ -270,6 +290,7 @@ analyze_rel(Oid relid, RangeVar *relation,
 	 */
 	relation_close(onerel, NoLock);
 
+out:
 	pgstat_progress_end_command();
 }
 
@@ -368,16 +389,10 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 	starttime = GetCurrentTimestamp();
 
 	/*
-	 * Determine which columns to analyze
-	 *
-	 * Note that system attributes are never analyzed, so we just reject them
-	 * at the lookup stage.  We also reject duplicate column mentions.  (We
-	 * could alternatively ignore duplicates, but analyzing a column twice
-	 * won't work; we'd end up making a conflicting update in pg_statistic.)
+	 * Determine which columns to analyze.
 	 */
 	if (va_cols != NIL)
 	{
-		Bitmapset  *unique_cols = NULL;
 		ListCell   *le;
 
 		vacattrstats = (VacAttrStats **) palloc(list_length(va_cols) *
@@ -388,18 +403,7 @@ do_analyze_rel(Relation onerel, const VacuumParams *params,
 			char	   *col = strVal(lfirst(le));
 
 			i = attnameAttNum(onerel, col, false);
-			if (i == InvalidAttrNumber)
-				ereport(ERROR,
-						(errcode(ERRCODE_UNDEFINED_COLUMN),
-						 errmsg("column \"%s\" of relation \"%s\" does not exist",
-								col, RelationGetRelationName(onerel))));
-			if (bms_is_member(i, unique_cols))
-				ereport(ERROR,
-						(errcode(ERRCODE_DUPLICATE_COLUMN),
-						 errmsg("column \"%s\" of relation \"%s\" appears more than once",
-								col, RelationGetRelationName(onerel))));
-			unique_cols = bms_add_member(unique_cols, i);
-
+			Assert(i != InvalidAttrNumber);
 			vacattrstats[tcnt] = examine_attribute(onerel, i, NULL);
 			if (vacattrstats[tcnt] != NULL)
 				tcnt++;
@@ -1030,6 +1034,40 @@ compute_index_stats(Relation onerel, double totalrows,
 	MemoryContextDelete(ind_context);
 }
 
+/*
+ * validate_va_cols_list -- validate the columns list given to analyze_rel
+ *
+ * Note that system attributes are never analyzed, so we just reject them at
+ * the lookup stage.  We also reject duplicate column mentions.  (We could
+ * alternatively ignore duplicates, but analyzing a column twice won't work;
+ * we'd end up making a conflicting update in pg_statistic.)
+ */
+static void
+validate_va_cols_list(Relation onerel, List *va_cols)
+{
+	Bitmapset  *unique_cols = NULL;
+	ListCell   *le;
+
+	Assert(va_cols != NIL);
+	foreach(le, va_cols)
+	{
+		char	   *col = strVal(lfirst(le));
+		int			i = attnameAttNum(onerel, col, false);
+
+		if (i == InvalidAttrNumber)
+			ereport(ERROR,
+					(errcode(ERRCODE_UNDEFINED_COLUMN),
+					 errmsg("column \"%s\" of relation \"%s\" does not exist",
+							col, RelationGetRelationName(onerel))));
+		if (bms_is_member(i, unique_cols))
+			ereport(ERROR,
+					(errcode(ERRCODE_DUPLICATE_COLUMN),
+					 errmsg("column \"%s\" of relation \"%s\" appears more than once",
+							col, RelationGetRelationName(onerel))));
+		unique_cols = bms_add_member(unique_cols, i);
+	}
+}
+
 /*
  * examine_attribute -- pre-analysis of a single column
  *
@@ -1044,37 +1082,15 @@ examine_attribute(Relation onerel, int attnum, Node *index_expr)
 {
 	Form_pg_attribute attr = TupleDescAttr(onerel->rd_att, attnum - 1);
 	int			attstattarget;
-	HeapTuple	atttuple;
-	Datum		dat;
-	bool		isnull;
 	HeapTuple	typtuple;
 	VacAttrStats *stats;
 	int			i;
 	bool		ok;
 
-	/* Never analyze dropped columns */
-	if (attr->attisdropped)
-		return NULL;
-
-	/* Don't analyze virtual generated columns */
-	if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
-		return NULL;
-
 	/*
-	 * Get attstattarget value.  Set to -1 if null.  (Analyze functions expect
-	 * -1 to mean use default_statistics_target; see for example
-	 * std_typanalyze.)
+	 * Check if the column is analyzable.
 	 */
-	atttuple = SearchSysCache2(ATTNUM, ObjectIdGetDatum(RelationGetRelid(onerel)), Int16GetDatum(attnum));
-	if (!HeapTupleIsValid(atttuple))
-		elog(ERROR, "cache lookup failed for attribute %d of relation %u",
-			 attnum, RelationGetRelid(onerel));
-	dat = SysCacheGetAttr(ATTNUM, atttuple, Anum_pg_attribute_attstattarget, &isnull);
-	attstattarget = isnull ? -1 : DatumGetInt16(dat);
-	ReleaseSysCache(atttuple);
-
-	/* Don't analyze column if user has specified not to */
-	if (attstattarget == 0)
+	if (!attribute_is_analyzable(onerel, attnum, attr, &attstattarget))
 		return NULL;
 
 	/*
@@ -1155,6 +1171,45 @@ examine_attribute(Relation onerel, int attnum, Node *index_expr)
 	return stats;
 }
 
+bool
+attribute_is_analyzable(Relation onerel, int attnum, Form_pg_attribute attr,
+						int *p_attstattarget)
+{
+	int			attstattarget;
+	HeapTuple	atttuple;
+	Datum		dat;
+	bool		isnull;
+
+	/* Never analyze dropped columns */
+	if (attr->attisdropped)
+		return false;
+
+	/* Don't analyze virtual generated columns */
+	if (attr->attgenerated == ATTRIBUTE_GENERATED_VIRTUAL)
+		return false;
+
+	/*
+	 * Get attstattarget value.  Set to -1 if null.  (Analyze functions expect
+	 * -1 to mean use default_statistics_target; see for example
+	 * std_typanalyze.)
+	 */
+	atttuple = SearchSysCache2(ATTNUM, ObjectIdGetDatum(RelationGetRelid(onerel)), Int16GetDatum(attnum));
+	if (!HeapTupleIsValid(atttuple))
+		elog(ERROR, "cache lookup failed for attribute %d of relation %u",
+			 attnum, RelationGetRelid(onerel));
+	dat = SysCacheGetAttr(ATTNUM, atttuple, Anum_pg_attribute_attstattarget, &isnull);
+	attstattarget = isnull ? -1 : DatumGetInt16(dat);
+	ReleaseSysCache(atttuple);
+
+	/* Don't analyze column if user has specified not to */
+	if (attstattarget == 0)
+		return false;
+
+	if (p_attstattarget)
+		*p_attstattarget = attstattarget;
+	return true;
+}
+
 /*
  * Read stream callback returning the next BlockNumber as chosen by the
  * BlockSampling algorithm.
diff --git a/src/backend/statistics/extended_stats.c b/src/backend/statistics/extended_stats.c
index cb7898b21ee..2b83355d26e 100644
--- a/src/backend/statistics/extended_stats.c
+++ b/src/backend/statistics/extended_stats.c
@@ -246,6 +246,40 @@ BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
 	table_close(pg_stext, RowExclusiveLock);
 }
 
+/*
+ * Test if the given relation has extended statistics objects.
+ */
+bool
+HasRelationExtStatistics(Relation onerel)
+{
+	Relation	pg_statext;
+	SysScanDesc scan;
+	ScanKeyData skey;
+	bool		found;
+
+	pg_statext = table_open(StatisticExtRelationId, RowExclusiveLock);
+
+	/*
+	 * Prepare to scan pg_statistic_ext for entries having stxrelid = this
+	 * rel.
+	 */
+	ScanKeyInit(&skey,
+				Anum_pg_statistic_ext_stxrelid,
+				BTEqualStrategyNumber, F_OIDEQ,
+				ObjectIdGetDatum(RelationGetRelid(onerel)));
+
+	scan = systable_beginscan(pg_statext, StatisticExtRelidIndexId, true,
+							  NULL, 1, &skey);
+
+	found = HeapTupleIsValid(systable_getnext(scan));
+
+	systable_endscan(scan);
+
+	table_close(pg_statext, RowExclusiveLock);
+
+	return found;
+}
+
 /*
  * ComputeExtStatisticsRows
  *		Compute number of rows required by extended statistics on a table.
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 69fec07491b..956d9cea36d 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -430,6 +430,8 @@ extern void parallel_vacuum_main(dsm_segment *seg, shm_toc *toc);
 extern void analyze_rel(Oid relid, RangeVar *relation,
 						const VacuumParams *params, List *va_cols, bool in_outer_xact,
 						BufferAccessStrategy bstrategy);
+extern bool attribute_is_analyzable(Relation onerel, int attnum, Form_pg_attribute attr,
+									int *p_attstattarget);
 extern bool std_typanalyze(VacAttrStats *stats);
 
 /* in utils/misc/sampling.c --- duplicate of declarations in utils/sampling.h */
diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h
index 96b6f692d2a..abf59a0d8ad 100644
--- a/src/include/foreign/fdwapi.h
+++ b/src/include/foreign/fdwapi.h
@@ -157,6 +157,10 @@ typedef bool (*AnalyzeForeignTable_function) (Relation relation,
 											  AcquireSampleRowsFunc *func,
 											  BlockNumber *totalpages);
 
+typedef bool (*ImportForeignStatistics_function) (Relation relation,
+												  List *va_cols,
+												  int elevel);
+
 typedef List *(*ImportForeignSchema_function) (ImportForeignSchemaStmt *stmt,
 											   Oid serverOid);
 
@@ -255,6 +259,7 @@ typedef struct FdwRoutine
 
 	/* Support functions for ANALYZE */
 	AnalyzeForeignTable_function AnalyzeForeignTable;
+	ImportForeignStatistics_function ImportForeignStatistics;
 
 	/* Support functions for IMPORT FOREIGN SCHEMA */
 	ImportForeignSchema_function ImportForeignSchema;
diff --git a/src/include/statistics/statistics.h b/src/include/statistics/statistics.h
index 38a56f6ccb3..8f9b9d237fd 100644
--- a/src/include/statistics/statistics.h
+++ b/src/include/statistics/statistics.h
@@ -101,6 +101,7 @@ extern MCVList *statext_mcv_load(Oid mvoid, bool inh);
 extern void BuildRelationExtStatistics(Relation onerel, bool inh, double totalrows,
 									   int numrows, HeapTuple *rows,
 									   int natts, VacAttrStats **vacattrstats);
+extern bool HasRelationExtStatistics(Relation onerel);
 extern int	ComputeExtStatisticsRows(Relation onerel,
 									 int natts, VacAttrStats **vacattrstats);
 extern bool statext_is_kind_built(HeapTuple htup, char type);
