Add support for importing statistics from remote servers.

Add a new FDW callback routine that allows importing remote statistics
for a foreign table directly to the local server, instead of collecting
statistics locally.  The new callback routine is called at the beginning
of the ANALYZE operation on the table, and if the FDW failed to import
the statistics, the existing callback routine is called on the table to
collect statistics locally.

Also implement this for postgres_fdw.  It is enabled by "restore_stats"
option both at the server and table level.  Currently, it is the user's
responsibility to ensure remote statistics to import are up-to-date, so
the default is false.

Author: Corey Huinker <corey.huinker@gmail.com>
Co-authored-by: Etsuro Fujita <etsuro.fujita@gmail.com>
Reviewed-by: Ashutosh Bapat <ashutosh.bapat.oss@gmail.com>
Reviewed-by: Matheus Alcantara <matheusssilv97@gmail.com>
Reviewed-by: Chao Li <li.evan.chao@gmail.com>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Reviewed-by: Etsuro Fujita <etsuro.fujita@gmail.com>
Discussion: https://postgr.es/m/CADkLM%3DchrYAx%3DX2KUcDRST4RLaRLivYDohZrkW4LLBa0iBhb5w%40mail.gmail.com
This commit is contained in:
Etsuro Fujita 2026-04-08 19:15:00 +09:00
parent d1c01b79d4
commit 28972b6fc3
13 changed files with 1303 additions and 80 deletions

View file

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

View file

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

View file

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

View file

@ -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;
/* Pairs of remote columns with local columns */
typedef struct
{
AttrNumber local_attnum;
char local_attname[NAMEDATALEN];
char remote_attname[NAMEDATALEN];
int res_index;
} RemoteAttributeMapping;
/* Result sets that are returned from a foreign statistics scan */
typedef struct
{
PGresult *rel;
PGresult *att;
int server_version_num;
} RemoteStatsResults;
/* 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;
schemaname = get_namespace_name(RelationGetNamespace(relation));
relname = RelationGetRelationName(relation);
table = GetForeignTable(RelationGetRelid(relation));
server = GetForeignServer(table->serverid);
/*
* 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 *lc;
if (va_cols == NIL)
return true;
foreach(lc, va_cols)
{
char *col = strVal(lfirst(lc));
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
*/

View file

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

View file

@ -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 function imports the statistics successfully, it should return
<literal>true</literal>. Otherwise, return <literal>false</literal>, in
which case <function>AnalyzeForeignTable</function> callback function is
called on the foreign table to collect statistics locally, if supported.
</para>
<para>
For reference, the logic for calculating statistics in
<productname>PostgreSQL</productname> is 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 remotely-calculated statistics for
any tables, the <function>ImportForeignStatistics</function> pointer can be
set to <literal>NULL</literal>.
</para>
</sect2>
<sect2 id="fdw-callbacks-import">

View file

@ -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 fetch and restore remote statistics first.
</para>
</listitem>
</varlistentry>
</variablelist>
</sect3>

View file

@ -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,28 @@ 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 +220,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 +263,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 +291,7 @@ analyze_rel(Oid relid, RangeVar *relation,
*/
relation_close(onerel, NoLock);
out:
pgstat_progress_end_command();
}
@ -368,16 +390,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 +404,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 +1035,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 +1083,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 +1172,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.

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ ASN1_OBJECT
ASN1_OCTET_STRING
ASN1_STRING
ATAlterConstraint
AttrResultArgMap
AV
A_ArrayExpr
A_Const
@ -4218,9 +4219,11 @@ relopt_ternary
relopt_type
relopt_value
relopts_validator
RemoteAttributeMapping
remoteConn
remoteConnHashEnt
remoteDep
RemoteStatsResults
remove_nulling_relids_context
rendezvousHashEntry
rep