From 8a7bababfd39b40e32f8fff7ef1a8989138ed3e6 Mon Sep 17 00:00:00 2001 From: Gleb Kashkin Date: Tue, 22 Sep 2026 17:29:43 +0300 Subject: [PATCH 2/2] postgres_fdw: cache parameterized-path cost estimates In use_remote_estimate mode, every estimate for a parameterized scan costs a remote EXPLAIN, and with the new ReparameterizeForeignPath callback the planner may ask for the same parameterization repeatedly: once in postgresGetForeignPaths(), and later once per surviving path of the relation from get_cheapest_parameterized_child_path(). Remember the estimates in a per-relation list keyed by the (interned) ParamPathInfo, so that each parameterization is costed at most once per relation. In the common case, where the parameterized path was already built and merely lost the add_path() tournament, reparameterizing it now issues no remote queries at all. --- contrib/postgres_fdw/postgres_fdw.c | 105 +++++++++++++++++++++++----- contrib/postgres_fdw/postgres_fdw.h | 7 ++ 2 files changed, 93 insertions(+), 19 deletions(-) diff --git a/contrib/postgres_fdw/postgres_fdw.c b/contrib/postgres_fdw/postgres_fdw.c index 929dd129480..cea5c0811f8 100644 --- a/contrib/postgres_fdw/postgres_fdw.c +++ b/contrib/postgres_fdw/postgres_fdw.c @@ -278,6 +278,23 @@ typedef struct PgFdwAnalyzeState MemoryContext temp_cxt; /* context for per-tuple temporary data */ } PgFdwAnalyzeState; +/* + * Cached size and cost estimate for a parameterized scan of a base relation, + * keyed by its (interned) ParamPathInfo. These are kept in the + * param_path_costs list of the relation's PgFdwRelationInfo, so that + * reparameterizing a path does not require a second remote EXPLAIN for a + * parameterization we already costed. + */ +typedef struct PgFdwParamPathCost +{ + ParamPathInfo *param_info; + double rows; + int width; + int disabled_nodes; + Cost startup_cost; + Cost total_cost; +} PgFdwParamPathCost; + /* * This enum describes what's kept in the fdw_private list for a ForeignPath. * We store: @@ -611,6 +628,12 @@ static void postgresForeignAsyncNotify(AsyncRequest *areq); /* * Helper functions */ +static void get_param_path_cost(PlannerInfo *root, + RelOptInfo *baserel, + ParamPathInfo *param_info, + double *p_rows, int *p_width, + int *p_disabled_nodes, + Cost *p_startup_cost, Cost *p_total_cost); static void estimate_path_cost_size(PlannerInfo *root, RelOptInfo *foreignrel, List *param_join_conds, @@ -880,6 +903,9 @@ postgresGetForeignRelSize(PlannerInfo *root, apply_server_options(fpinfo); apply_table_options(fpinfo); + /* No parameterized-path estimates cached yet. */ + fpinfo->param_path_costs = NIL; + /* * If the table or the server is configured to use remote estimates, * identify which user to do remote access as during planning. This @@ -1416,17 +1442,10 @@ postgresGetForeignPaths(PlannerInfo *root, Cost startup_cost; Cost total_cost; - /* Get a cost estimate from the remote */ - estimate_path_cost_size(root, baserel, - param_info->ppi_clauses, NIL, NULL, - &rows, &width, &disabled_nodes, - &startup_cost, &total_cost); - - /* - * ppi_rows currently won't get looked at by anything, but still we - * may as well ensure that it matches our idea of the rowcount. - */ - param_info->ppi_rows = rows; + /* Get a cost estimate from the remote (or from our cache) */ + get_param_path_cost(root, baserel, param_info, + &rows, &width, &disabled_nodes, + &startup_cost, &total_cost); /* Make the path */ path = create_foreignscan_path(root, baserel, @@ -1444,6 +1463,55 @@ postgresGetForeignPaths(PlannerInfo *root, } } +/* + * get_param_path_cost + * Estimate the size and cost of scanning baserel with the join clauses + * of the given ParamPathInfo pushed down, caching the result. + */ +static void +get_param_path_cost(PlannerInfo *root, RelOptInfo *baserel, + ParamPathInfo *param_info, + double *p_rows, int *p_width, int *p_disabled_nodes, + Cost *p_startup_cost, Cost *p_total_cost) +{ + PgFdwRelationInfo *fpinfo = (PgFdwRelationInfo *) baserel->fdw_private; + PgFdwParamPathCost *ppc = NULL; + ListCell *lc; + + foreach(lc, fpinfo->param_path_costs) + { + PgFdwParamPathCost *cached = (PgFdwParamPathCost *) lfirst(lc); + + if (cached->param_info == param_info) + { + ppc = cached; + break; + } + } + + if (ppc == NULL) + { + ppc = (PgFdwParamPathCost *) palloc(sizeof(PgFdwParamPathCost)); + ppc->param_info = param_info; + + /* Get a cost estimate from the remote */ + estimate_path_cost_size(root, baserel, + param_info->ppi_clauses, NIL, NULL, + &ppc->rows, &ppc->width, &ppc->disabled_nodes, + &ppc->startup_cost, &ppc->total_cost); + + param_info->ppi_rows = ppc->rows; + + fpinfo->param_path_costs = lappend(fpinfo->param_path_costs, ppc); + } + + *p_rows = ppc->rows; + *p_width = ppc->width; + *p_disabled_nodes = ppc->disabled_nodes; + *p_startup_cost = ppc->startup_cost; + *p_total_cost = ppc->total_cost; +} + /* * postgresReparameterizeForeignPath * Build a version of a base-relation foreign scan path that is @@ -1482,20 +1550,19 @@ postgresReparameterizeForeignPath(PlannerInfo *root, ForeignPath *path, /* * Note that we ignore the given path's pathkeys and always produce an * unsorted path. A parameterized path is only ever used on the inside - * of a NestLoop, where its ordering is of no interest. + * of a NestLoop, where its ordering is of no interest. Producing an + * unsorted path also means that we get to reuse the cost estimate we + * already made for this parameterization, if any. */ param_info = get_baserel_parampathinfo(root, baserel, required_outer); if (param_info == NULL) return NULL; /* shouldn't happen */ - /* Get a cost estimate from the remote */ - estimate_path_cost_size(root, baserel, - param_info->ppi_clauses, NIL, NULL, - &rows, &width, &disabled_nodes, - &startup_cost, &total_cost); - - param_info->ppi_rows = rows; + /* Get a cost estimate from the remote (or from our cache) */ + get_param_path_cost(root, baserel, param_info, + &rows, &width, &disabled_nodes, + &startup_cost, &total_cost); return (Path *) create_foreignscan_path(root, baserel, NULL, /* default pathtarget */ diff --git a/contrib/postgres_fdw/postgres_fdw.h b/contrib/postgres_fdw/postgres_fdw.h index a2bb1ff352c..9d61c0216cf 100644 --- a/contrib/postgres_fdw/postgres_fdw.h +++ b/contrib/postgres_fdw/postgres_fdw.h @@ -75,6 +75,13 @@ typedef struct PgFdwRelationInfo Cost rel_startup_cost; Cost rel_total_cost; + /* + * Cached estimates for parameterized scans of a base relation, one + * entry per ParamPathInfo (list of PgFdwParamPathCost). Only used in + * use_remote_estimate mode. + */ + List *param_path_costs; + /* Options extracted from catalogs. */ bool use_remote_estimate; Cost fdw_startup_cost; -- 2.55.0