From 5dc6c77c6ed511c1b68ec196537647899ee36c8d Mon Sep 17 00:00:00 2001 From: "Chao Li (Evan)" Date: Fri, 12 Jun 2026 11:20:19 +0800 Subject: [PATCH v1] Fix ANALYZE reporting after imported foreign-table stats Commit 28972b6fc added the ImportForeignStatistics FDW callback, which lets ANALYZE import remote statistics instead of sampling a foreign table. When that path succeeds, analyze_rel() skips do_analyze_rel(). That also skips pgstat_report_analyze(), so cumulative ANALYZE stats such as analyze_count, last_analyze_time, and live_tuples are not updated even though ANALYZE succeeded. Fix by extending the ImportForeignStatistics callback to return the imported live-row estimate, and have analyze_rel() report ANALYZE to the cumulative stats system after a successful import. For postgres_fdw, the returned row estimate comes from the imported remote reltuples value. Since imported relation stats do not include a dead-tuple estimate, analyze_rel() reports zero dead tuples for this path. Add regression coverage for postgres_fdw stats import to verify that a successful imported ANALYZE updates live_tuples, analyze_count, and last_analyze_time. Author: Chao Li Reported-by: Discussion: https://postgr.es/m/ --- .../postgres_fdw/expected/postgres_fdw.out | 20 +++++++++++++++++++ contrib/postgres_fdw/postgres_fdw.c | 11 ++++++++-- contrib/postgres_fdw/sql/postgres_fdw.sql | 5 +++++ doc/src/sgml/fdwhandler.sgml | 12 ++++++----- src/backend/commands/analyze.c | 20 +++++++++++++++---- src/include/foreign/fdwapi.h | 3 ++- 6 files changed, 59 insertions(+), 12 deletions(-) diff --git a/contrib/postgres_fdw/expected/postgres_fdw.out b/contrib/postgres_fdw/expected/postgres_fdw.out index e90289e4ab1..c16658966d7 100644 --- a/contrib/postgres_fdw/expected/postgres_fdw.out +++ b/contrib/postgres_fdw/expected/postgres_fdw.out @@ -12888,9 +12888,29 @@ 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; +SELECT pg_stat_reset_single_table_counters('simport_ftable'::regclass); + pg_stat_reset_single_table_counters +------------------------------------- + +(1 row) + 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" +SELECT pg_stat_force_next_flush(); + pg_stat_force_next_flush +-------------------------- + +(1 row) + +SELECT pg_stat_get_live_tuples('simport_ftable'::regclass), + pg_stat_get_analyze_count('simport_ftable'::regclass), + pg_stat_get_last_analyze_time('simport_ftable'::regclass) IS NOT NULL; + pg_stat_get_live_tuples | pg_stat_get_analyze_count | ?column? +-------------------------+---------------------------+---------- + 4 | 1 | t +(1 row) + 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" diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 0a589f8db74..7562677fc1d 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -336,6 +336,7 @@ typedef struct PGresult *rel; PGresult *att; int server_version_num; + double reltuples; } RemoteStatsResults; /* Column order in relation stats query */ @@ -584,7 +585,8 @@ static bool postgresAnalyzeForeignTable(Relation relation, BlockNumber *totalpages); static bool postgresImportForeignStatistics(Relation relation, List *va_cols, - int elevel); + int elevel, + double *totalrows); static List *postgresImportForeignSchema(ImportForeignSchemaStmt *stmt, Oid serverOid); static void postgresGetForeignJoinPaths(PlannerInfo *root, @@ -5588,7 +5590,8 @@ analyze_row_processor(PGresult *res, int row, PgFdwAnalyzeState *astate) * Attempt to fetch/restore remote statistics instead of sampling. */ static bool -postgresImportForeignStatistics(Relation relation, List *va_cols, int elevel) +postgresImportForeignStatistics(Relation relation, List *va_cols, int elevel, + double *totalrows) { const char *schemaname = NULL; const char *relname = NULL; @@ -5665,9 +5668,12 @@ postgresImportForeignStatistics(Relation relation, List *va_cols, int elevel) attrcnt, remattrmap, &remstats); if (ok) + { + *totalrows = remstats.reltuples; ereport(elevel, (errmsg("finished importing statistics for foreign table \"%s.%s\"", schemaname, relname))); + } PQclear(remstats.rel); PQclear(remstats.att); @@ -5770,6 +5776,7 @@ fetch_remote_statistics(Relation relation, * to sampling. */ reltuples = strtod(PQgetvalue(relstats, 0, RELSTATS_RELTUPLES), NULL); + remstats->reltuples = reltuples; if (((server_version_num < 140000) && (reltuples == 0)) || ((server_version_num >= 140000) && (reltuples == -1))) { diff --git a/contrib/postgres_fdw/sql/postgres_fdw.sql b/contrib/postgres_fdw/sql/postgres_fdw.sql index dfc58beb0d2..494b97c738e 100644 --- a/contrib/postgres_fdw/sql/postgres_fdw.sql +++ b/contrib/postgres_fdw/sql/postgres_fdw.sql @@ -4546,7 +4546,12 @@ ANALYZE simport_ftable; -- should fail ALTER TABLE simport_table ALTER COLUMN c2 SET STATISTICS DEFAULT; ANALYZE simport_table; +SELECT pg_stat_reset_single_table_counters('simport_ftable'::regclass); ANALYZE VERBOSE simport_ftable; -- should work +SELECT pg_stat_force_next_flush(); +SELECT pg_stat_get_live_tuples('simport_ftable'::regclass), + pg_stat_get_analyze_count('simport_ftable'::regclass), + pg_stat_get_last_analyze_time('simport_ftable'::regclass) IS NOT NULL; ANALYZE VERBOSE simport_ftable (c1); -- should work diff --git a/doc/src/sgml/fdwhandler.sgml b/doc/src/sgml/fdwhandler.sgml index 8685a078c52..207264529cf 100644 --- a/doc/src/sgml/fdwhandler.sgml +++ b/doc/src/sgml/fdwhandler.sgml @@ -1414,7 +1414,8 @@ AcquireSampleRowsFunc(Relation relation, bool ImportForeignStatistics(Relation relation, List *va_cols, - int elevel); + int elevel, + double *totalrows); This function is called before the AnalyzeForeignTable @@ -1426,10 +1427,11 @@ ImportForeignStatistics(Relation relation, describing the target foreign table. va_cols, if not NIL, contains the columns specified in the ANALYZE command. elevel contains a flag indicating a logging - level to use. - If the function imports the statistics successfully, it should return - true. Otherwise, return false, in - which case AnalyzeForeignTable callback function is + level to use. If the function imports the statistics successfully, it + should store the estimated number of live rows for the foreign table into + totalrows and return true. + Otherwise, return false, in which case + AnalyzeForeignTable callback function is called on the foreign table to collect statistics locally, if supported. diff --git a/src/backend/commands/analyze.c b/src/backend/commands/analyze.c index f66e80b757c..d6625d4057e 100644 --- a/src/backend/commands/analyze.c +++ b/src/backend/commands/analyze.c @@ -227,10 +227,22 @@ analyze_rel(Oid relid, RangeVar *relation, fdwroutine = GetFdwRoutineForRelation(onerel, false); - if (fdwroutine->ImportForeignStatistics != NULL && - fdwroutine->ImportForeignStatistics(onerel, va_cols, elevel)) - stats_imported = true; - else + if (fdwroutine->ImportForeignStatistics != NULL) + { + double imported_totalrows = 0; + TimestampTz import_starttime = GetCurrentTimestamp(); + + if (fdwroutine->ImportForeignStatistics(onerel, va_cols, elevel, + &imported_totalrows)) + { + pgstat_report_analyze(onerel, imported_totalrows, 0, + (va_cols == NIL), import_starttime); + + stats_imported = true; + } + } + + if (!stats_imported) { bool ok = false; diff --git a/src/include/foreign/fdwapi.h b/src/include/foreign/fdwapi.h index abf59a0d8ad..bd38d5e6db7 100644 --- a/src/include/foreign/fdwapi.h +++ b/src/include/foreign/fdwapi.h @@ -159,7 +159,8 @@ typedef bool (*AnalyzeForeignTable_function) (Relation relation, typedef bool (*ImportForeignStatistics_function) (Relation relation, List *va_cols, - int elevel); + int elevel, + double *totalrows); typedef List *(*ImportForeignSchema_function) (ImportForeignSchemaStmt *stmt, Oid serverOid); -- 2.50.1 (Apple Git-155)