From 8a7bababfd39b40e32f8fff7ef1a8989138ed3e6 Mon Sep 17 00:00:00 2001
From: Gleb Kashkin <g.kashkin@postgrespro.ru>
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

