From 478173538e0b315b434b7fd8fae7ea8479476b77 Mon Sep 17 00:00:00 2001
From: Rui Zhao <zhaorui126@gmail.com>
Date: Fri, 7 Aug 2026 23:33:58 +0800
Subject: [PATCH 2/2] Charge the Bloom filter probe cost on the rows actually
 probed

apply_expected_filters() charged

	path->total_cost += probes * BLOOM_FILTER_PROBE_COST * path->rows;

but path->rows is the path's output count, i.e. after the scan's own quals.
ExecScanExtended() probes the filters before evaluating those quals, so a probe
is paid for every tuple the scan fetches, including the ones the qual is about
to drop.  The charge is therefore too small by the quals' selectivity.

Each scan cost function now takes the filters and folds the probe cost into the
cpu_per_tuple it already charges on the right count:

	cost_seqscan, cost_samplescan       baserel->tuples
	cost_index, cost_bitmap_heap_scan   tuples_fetched
	cost_tidscan, cost_tidrangescan     ntuples

The filters are passed as an explicit argument rather than read back out of the
path, because bitmap_scan_cost_est() costs a stack-allocated BitmapHeapPath
with only a few fields filled in -- the same reason param_info is passed that
way.

create_filtered_scan_path() now re-costs its IndexPath clone with cost_index()
instead of adjusting the copied costs.  cost_index() recomputes only costs, so
the copied indexclauses and pathkeys are reused as before.

apply_expected_filters() stays for CustomScan providers, whose scans core
cannot cost, and its comment now says what it cannot account for.

join.out is updated for the one plan this changes, the fkest query, where the
filter is no longer chosen: the scan there keeps 100 of 1000 rows, so v9 paid
for 100 probes where the executor performs 1000.  On that shape scaled to 1M
rows the filter costs more than it saves -- 195ms with it against 174ms
without -- because it probes every tuple to save an integer comparison on rows
the scan qual drops anyway.

make check stays 248/248 with that.
---
 src/backend/optimizer/path/costsize.c | 122 ++++++++++++++++++++++++--
 src/backend/optimizer/path/indxpath.c |   2 +
 src/backend/optimizer/util/pathnode.c |  86 ++++++++++++------
 src/include/optimizer/cost.h          |  21 +++--
 src/include/optimizer/pathnode.h      |   1 +
 src/test/regress/expected/join.out    |   4 +-
 6 files changed, 194 insertions(+), 42 deletions(-)

diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 7569b88057..0c29af4f9e 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -306,7 +306,8 @@ clamp_width_est(int64 tuple_width)
  */
 void
 cost_seqscan(Path *path, PlannerInfo *root,
-			 RelOptInfo *baserel, ParamPathInfo *param_info)
+			 RelOptInfo *baserel, ParamPathInfo *param_info,
+			 List *filters)
 {
 	Cost		startup_cost = 0;
 	Cost		cpu_run_cost;
@@ -326,6 +327,15 @@ cost_seqscan(Path *path, PlannerInfo *root,
 	else
 		path->rows = baserel->rows;
 
+	/*
+	 * Expected Bloom filters cut that down further.  The surviving fraction
+	 * is a property of the filter set, so paths of the same parameterization
+	 * expecting the same filters agree on their rows.
+	 */
+	if (filters != NIL)
+		path->rows = clamp_row_est(path->rows *
+								   expected_filters_selectivity(filters));
+
 	/* fetch estimated page cost for tablespace containing table */
 	get_tablespace_page_costs(baserel->reltablespace,
 							  NULL,
@@ -341,6 +351,15 @@ cost_seqscan(Path *path, PlannerInfo *root,
 
 	startup_cost += qpqual_cost.startup;
 	cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple;
+
+	/*
+	 * Pushed-down Bloom filters are probed before the scan's own quals (see
+	 * ExecScanExtended), so the probes are paid on every tuple fetched.
+	 */
+	if (filters != NIL)
+		cpu_per_tuple += expected_filters_probes(filters) *
+			BLOOM_FILTER_PROBE_COST;
+
 	cpu_run_cost = cpu_per_tuple * baserel->tuples;
 	/* tlist eval costs are paid per output row, not per tuple scanned */
 	startup_cost += path->pathtarget->cost.startup;
@@ -385,7 +404,8 @@ cost_seqscan(Path *path, PlannerInfo *root,
  */
 void
 cost_samplescan(Path *path, PlannerInfo *root,
-				RelOptInfo *baserel, ParamPathInfo *param_info)
+				RelOptInfo *baserel, ParamPathInfo *param_info,
+				List *filters)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -413,6 +433,15 @@ cost_samplescan(Path *path, PlannerInfo *root,
 	else
 		path->rows = baserel->rows;
 
+	/*
+	 * Expected Bloom filters cut that down further.  The surviving fraction
+	 * is a property of the filter set, so paths of the same parameterization
+	 * expecting the same filters agree on their rows.
+	 */
+	if (filters != NIL)
+		path->rows = clamp_row_est(path->rows *
+								   expected_filters_selectivity(filters));
+
 	/* fetch estimated page cost for tablespace containing table */
 	get_tablespace_page_costs(baserel->reltablespace,
 							  &spc_random_page_cost,
@@ -440,6 +469,15 @@ cost_samplescan(Path *path, PlannerInfo *root,
 
 	startup_cost += qpqual_cost.startup;
 	cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple;
+
+	/*
+	 * Pushed-down Bloom filters are probed before the scan's own quals (see
+	 * ExecScanExtended), so the probes are paid on every tuple fetched.
+	 */
+	if (filters != NIL)
+		cpu_per_tuple += expected_filters_probes(filters) *
+			BLOOM_FILTER_PROBE_COST;
+
 	run_cost += cpu_per_tuple * baserel->tuples;
 	/* tlist eval costs are paid per output row, not per tuple scanned */
 	startup_cost += path->pathtarget->cost.startup;
@@ -581,7 +619,7 @@ cost_gather_merge(GatherMergePath *path, PlannerInfo *root,
  */
 void
 cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
-		   bool partial_path)
+		   bool partial_path, List *filters)
 {
 	IndexOptInfo *index = path->indexinfo;
 	RelOptInfo *baserel = index->rel;
@@ -638,6 +676,15 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
 											  path->indexclauses);
 	}
 
+	/*
+	 * Expected Bloom filters cut that down further.  The surviving fraction
+	 * is a property of the filter set, so paths of the same parameterization
+	 * expecting the same filters agree on their rows.
+	 */
+	if (filters != NIL)
+		path->path.rows = clamp_row_est(path->path.rows *
+										expected_filters_selectivity(filters));
+
 	/* is this scan type disabled? */
 	enable_mask = (indexonly ? PGS_INDEXONLYSCAN : PGS_INDEXSCAN)
 		| (partial_path ? 0 : PGS_CONSIDER_NONPARTIAL);
@@ -834,6 +881,14 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count,
 	startup_cost += qpqual_cost.startup;
 	cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple;
 
+	/*
+	 * Pushed-down Bloom filters are probed before the scan's own quals (see
+	 * ExecScanExtended), so the probes are paid on every tuple fetched.
+	 */
+	if (filters != NIL)
+		cpu_per_tuple += expected_filters_probes(filters) *
+			BLOOM_FILTER_PROBE_COST;
+
 	cpu_run_cost += cpu_per_tuple * tuples_fetched;
 
 	/* tlist eval costs are paid per output row, not per tuple scanned */
@@ -1048,7 +1103,7 @@ get_indexpath_pages(Path *bitmapqual)
  */
 void
 cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
-					  ParamPathInfo *param_info,
+					  ParamPathInfo *param_info, List *filters,
 					  Path *bitmapqual, double loop_count)
 {
 	Cost		startup_cost = 0;
@@ -1076,6 +1131,15 @@ cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
 	else
 		path->rows = baserel->rows;
 
+	/*
+	 * Expected Bloom filters cut that down further.  The surviving fraction
+	 * is a property of the filter set, so paths of the same parameterization
+	 * expecting the same filters agree on their rows.
+	 */
+	if (filters != NIL)
+		path->rows = clamp_row_est(path->rows *
+								   expected_filters_selectivity(filters));
+
 	pages_fetched = compute_bitmap_pages(root, baserel, bitmapqual,
 										 loop_count, &indexTotalCost,
 										 &tuples_fetched);
@@ -1117,6 +1181,15 @@ cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
 
 	startup_cost += qpqual_cost.startup;
 	cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple;
+
+	/*
+	 * Pushed-down Bloom filters are probed before the scan's own quals (see
+	 * ExecScanExtended), so the probes are paid on every tuple fetched.
+	 */
+	if (filters != NIL)
+		cpu_per_tuple += expected_filters_probes(filters) *
+			BLOOM_FILTER_PROBE_COST;
+
 	cpu_run_cost = cpu_per_tuple * tuples_fetched;
 
 	/* Adjust costing for parallelism, if used. */
@@ -1287,7 +1360,8 @@ cost_bitmap_or_node(BitmapOrPath *path, PlannerInfo *root)
  */
 void
 cost_tidscan(Path *path, PlannerInfo *root,
-			 RelOptInfo *baserel, List *tidquals, ParamPathInfo *param_info)
+			 RelOptInfo *baserel, List *tidquals, ParamPathInfo *param_info,
+			 List *filters)
 {
 	Cost		startup_cost = 0;
 	Cost		run_cost = 0;
@@ -1310,6 +1384,15 @@ cost_tidscan(Path *path, PlannerInfo *root,
 	else
 		path->rows = baserel->rows;
 
+	/*
+	 * Expected Bloom filters cut that down further.  The surviving fraction
+	 * is a property of the filter set, so paths of the same parameterization
+	 * expecting the same filters agree on their rows.
+	 */
+	if (filters != NIL)
+		path->rows = clamp_row_est(path->rows *
+								   expected_filters_selectivity(filters));
+
 	/* Count how many tuples we expect to retrieve */
 	ntuples = 0;
 	foreach(l, tidquals)
@@ -1366,6 +1449,15 @@ cost_tidscan(Path *path, PlannerInfo *root,
 	startup_cost += qpqual_cost.startup + tid_qual_cost.per_tuple;
 	cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple -
 		tid_qual_cost.per_tuple;
+
+	/*
+	 * Pushed-down Bloom filters are probed before the scan's own quals (see
+	 * ExecScanExtended), so the probes are paid on every tuple fetched.
+	 */
+	if (filters != NIL)
+		cpu_per_tuple += expected_filters_probes(filters) *
+			BLOOM_FILTER_PROBE_COST;
+
 	run_cost += cpu_per_tuple * ntuples;
 
 	/* tlist eval costs are paid per output row, not per tuple scanned */
@@ -1398,7 +1490,7 @@ cost_tidscan(Path *path, PlannerInfo *root,
 void
 cost_tidrangescan(Path *path, PlannerInfo *root,
 				  RelOptInfo *baserel, List *tidrangequals,
-				  ParamPathInfo *param_info)
+				  ParamPathInfo *param_info, List *filters)
 {
 	Selectivity selectivity;
 	double		pages;
@@ -1424,6 +1516,15 @@ cost_tidrangescan(Path *path, PlannerInfo *root,
 	else
 		path->rows = baserel->rows;
 
+	/*
+	 * Expected Bloom filters cut that down further.  The surviving fraction
+	 * is a property of the filter set, so paths of the same parameterization
+	 * expecting the same filters agree on their rows.
+	 */
+	if (filters != NIL)
+		path->rows = clamp_row_est(path->rows *
+								   expected_filters_selectivity(filters));
+
 	/* Count how many tuples and pages we expect to scan */
 	selectivity = clauselist_selectivity(root, tidrangequals, baserel->relid,
 										 JOIN_INNER, NULL);
@@ -1470,6 +1571,15 @@ cost_tidrangescan(Path *path, PlannerInfo *root,
 	startup_cost = qpqual_cost.startup + tid_qual_cost.per_tuple;
 	cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple -
 		tid_qual_cost.per_tuple;
+
+	/*
+	 * Pushed-down Bloom filters are probed before the scan's own quals (see
+	 * ExecScanExtended), so the probes are paid on every tuple fetched.
+	 */
+	if (filters != NIL)
+		cpu_per_tuple += expected_filters_probes(filters) *
+			BLOOM_FILTER_PROBE_COST;
+
 	cpu_run_cost = cpu_per_tuple * ntuples;
 
 	/* tlist eval costs are paid per output row, not per tuple scanned */
diff --git a/src/backend/optimizer/path/indxpath.c b/src/backend/optimizer/path/indxpath.c
index a6c9a41ead..ea2e10e66c 100644
--- a/src/backend/optimizer/path/indxpath.c
+++ b/src/backend/optimizer/path/indxpath.c
@@ -2029,6 +2029,7 @@ bitmap_scan_cost_est(PlannerInfo *root, RelOptInfo *rel, Path *ipath)
 	bpath.path.parent = rel;
 	bpath.path.pathtarget = rel->reltarget;
 	bpath.path.param_info = ipath->param_info;
+	bpath.path.expected_filters = NIL;
 	bpath.path.pathkeys = NIL;
 	bpath.bitmapqual = ipath;
 
@@ -2041,6 +2042,7 @@ bitmap_scan_cost_est(PlannerInfo *root, RelOptInfo *rel, Path *ipath)
 	/* Now we can do cost_bitmap_heap_scan */
 	cost_bitmap_heap_scan(&bpath.path, root, rel,
 						  bpath.path.param_info,
+						  NIL,
 						  ipath,
 						  get_loop_count(root, rel->relid,
 										 PATH_REQ_OUTER(ipath)));
diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c
index 9be2b5b831..abeff95d82 100644
--- a/src/backend/optimizer/util/pathnode.c
+++ b/src/backend/optimizer/util/pathnode.c
@@ -449,14 +449,6 @@ expected_filters_selectivity(List *filters)
 	return sel;
 }
 
-/*
- * Estimated cost of probing one Bloom filter for one tuple.  A probe (a few
- * hash evaluations and bit tests) is cheaper than a full hash-table lookup,
- * so we charge less than cpu_operator_cost per probe; the bottom-up Bloom
- * filter paper likewise sets its per-filter probe cost below a hash probe.
- */
-#define BLOOM_FILTER_PROBE_COST (cpu_operator_cost * 0.5)
-
 /*
  * apply_expected_filters
  *	  Adjust an already-costed path to account for probing the given set of
@@ -475,16 +467,13 @@ expected_filters_selectivity(List *filters)
  * Callers that can do something smarter (e.g. a CustomScan skipping whole row
  * groups via a zone map) should cost themselves instead of using this.
  */
-void
-apply_expected_filters(Path *path, List *filters)
+double
+expected_filters_probes(List *filters)
 {
 	double		probes = 0.0;
 	double		surviving = 1.0;
 	ListCell   *lc;
 
-	if (filters == NIL)
-		return;
-
 	foreach(lc, filters)
 	{
 		ExpectedFilter *f = (ExpectedFilter *) lfirst(lc);
@@ -493,8 +482,31 @@ apply_expected_filters(Path *path, List *filters)
 		surviving *= f->selectivity;
 	}
 
-	path->total_cost += probes * BLOOM_FILTER_PROBE_COST * path->rows;
-	path->rows = clamp_row_est(path->rows * surviving);
+	return probes;
+}
+
+/*
+ * apply_expected_filters
+ *	  Attach expected filters to a path costed outside core -- a CustomScan
+ *	  provider's -- and adjust its estimates for them.
+ *
+ * Core's own scan cost functions take the filters as an argument and apply
+ * them themselves, charging the probes on the count of tuples they fetch.
+ * We cannot do that for a path we did not cost, so the probes are charged on
+ * path->rows, which understates them if the provider's scan has quals of its
+ * own.  A provider that knows better should cost its path itself and just
+ * set expected_filters.
+ */
+void
+apply_expected_filters(Path *path, List *filters)
+{
+	if (filters == NIL)
+		return;
+
+	path->total_cost += expected_filters_probes(filters) *
+		BLOOM_FILTER_PROBE_COST * path->rows;
+	path->rows = clamp_row_est(path->rows *
+							   expected_filters_selectivity(filters));
 	path->expected_filters = filters;
 }
 
@@ -529,7 +541,19 @@ create_filtered_scan_path(PlannerInfo *root, Path *subpath, List *filters)
 	newpath = (IndexPath *) palloc(sizeof(IndexPath));
 	memcpy(newpath, subpath, sizeof(IndexPath));
 
-	apply_expected_filters(&newpath->path, filters);
+	newpath->path.expected_filters = filters;
+
+	/*
+	 * Re-cost rather than adjusting the copied costs: cost_index() takes the
+	 * filters like the other scan cost functions do, and charges the probes
+	 * on the tuples the index fetches.  It recomputes only costs -- it reuses
+	 * the indexclauses and pathkeys we just copied.
+	 *
+	 * generate_expected_filter_paths() only offers us unparameterized paths,
+	 * so the loop count is 1.0 (see get_loop_count).
+	 */
+	Assert(subpath->param_info == NULL);
+	cost_index(newpath, root, 1.0, subpath->parallel_workers > 0, filters);
 
 	return (Path *) newpath;
 }
@@ -1401,8 +1425,9 @@ create_seqscan_path(PlannerInfo *root, RelOptInfo *rel,
 	pathnode->parallel_workers = parallel_workers;
 	pathnode->pathkeys = NIL;	/* seqscan has unordered result */
 
-	cost_seqscan(pathnode, root, rel, pathnode->param_info);
-	apply_expected_filters(pathnode, filters);
+	pathnode->expected_filters = filters;
+
+	cost_seqscan(pathnode, root, rel, pathnode->param_info, filters);
 
 	return pathnode;
 }
@@ -1427,8 +1452,9 @@ create_samplescan_path(PlannerInfo *root, RelOptInfo *rel,
 	pathnode->parallel_workers = 0;
 	pathnode->pathkeys = NIL;	/* samplescan has unordered result */
 
-	cost_samplescan(pathnode, root, rel, pathnode->param_info);
-	apply_expected_filters(pathnode, filters);
+	pathnode->expected_filters = filters;
+
+	cost_samplescan(pathnode, root, rel, pathnode->param_info, filters);
 
 	return pathnode;
 }
@@ -1486,7 +1512,8 @@ create_index_path(PlannerInfo *root,
 	pathnode->indexorderbycols = indexorderbycols;
 	pathnode->indexscandir = indexscandir;
 
-	cost_index(pathnode, root, loop_count, partial_path);
+	cost_index(pathnode, root, loop_count, partial_path,
+			   pathnode->path.expected_filters);
 
 	/*
 	 * cost_index will set disabled_nodes to 1 if this rel is not allowed to
@@ -1534,10 +1561,12 @@ create_bitmap_heap_path(PlannerInfo *root,
 
 	pathnode->bitmapqual = bitmapqual;
 
+	pathnode->path.expected_filters = filters;
+
 	cost_bitmap_heap_scan(&pathnode->path, root, rel,
 						  pathnode->path.param_info,
+						  filters,
 						  bitmapqual, loop_count);
-	apply_expected_filters(&pathnode->path, filters);
 
 	return pathnode;
 }
@@ -1668,9 +1697,10 @@ create_tidscan_path(PlannerInfo *root, RelOptInfo *rel, List *tidquals,
 
 	pathnode->tidquals = tidquals;
 
+	pathnode->path.expected_filters = filters;
+
 	cost_tidscan(&pathnode->path, root, rel, tidquals,
-				 pathnode->path.param_info);
-	apply_expected_filters(&pathnode->path, filters);
+				 pathnode->path.param_info, filters);
 
 	return pathnode;
 }
@@ -1699,9 +1729,10 @@ create_tidrangescan_path(PlannerInfo *root, RelOptInfo *rel,
 
 	pathnode->tidrangequals = tidrangequals;
 
+	pathnode->path.expected_filters = filters;
+
 	cost_tidrangescan(&pathnode->path, root, rel, tidrangequals,
-					  pathnode->path.param_info);
-	apply_expected_filters(&pathnode->path, filters);
+					  pathnode->path.param_info, filters);
 
 	return pathnode;
 }
@@ -4335,7 +4366,8 @@ reparameterize_path(PlannerInfo *root, Path *path,
 				memcpy(newpath, ipath, sizeof(IndexPath));
 				newpath->path.param_info =
 					get_baserel_parampathinfo(root, rel, required_outer);
-				cost_index(newpath, root, loop_count, false);
+				cost_index(newpath, root, loop_count, false,
+						   newpath->path.expected_filters);
 				return (Path *) newpath;
 			}
 		case T_BitmapHeapScan:
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index 8ca9b48a55..c818a4dece 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -33,6 +33,14 @@
 #define DEFAULT_RECURSIVE_WORKTABLE_FACTOR  10.0
 #define DEFAULT_EFFECTIVE_CACHE_SIZE  524288	/* measured in pages */
 
+/*
+ * Estimated cost of probing one Bloom filter for one tuple.  A probe (a few
+ * hash evaluations and bit tests) is cheaper than a full hash-table lookup,
+ * so we charge less than cpu_operator_cost per probe; the bottom-up Bloom
+ * filter paper likewise sets its per-filter probe cost below a hash probe.
+ */
+#define BLOOM_FILTER_PROBE_COST (cpu_operator_cost * 0.5)
+
 typedef enum
 {
 	CONSTRAINT_EXCLUSION_OFF,	/* do not use c_e */
@@ -81,22 +89,23 @@ extern PGDLLIMPORT int constraint_exclusion;
 extern double index_pages_fetched(double tuples_fetched, BlockNumber pages,
 								  double index_pages, PlannerInfo *root);
 extern void cost_seqscan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
-						 ParamPathInfo *param_info);
+						 ParamPathInfo *param_info, List *filters);
 extern void cost_samplescan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
-							ParamPathInfo *param_info);
+							ParamPathInfo *param_info, List *filters);
 extern void cost_index(IndexPath *path, PlannerInfo *root,
-					   double loop_count, bool partial_path);
+					   double loop_count, bool partial_path, List *filters);
 extern void cost_bitmap_heap_scan(Path *path, PlannerInfo *root, RelOptInfo *baserel,
-								  ParamPathInfo *param_info,
+								  ParamPathInfo *param_info, List *filters,
 								  Path *bitmapqual, double loop_count);
 extern void cost_bitmap_and_node(BitmapAndPath *path, PlannerInfo *root);
 extern void cost_bitmap_or_node(BitmapOrPath *path, PlannerInfo *root);
 extern void cost_bitmap_tree_node(Path *path, Cost *cost, Selectivity *selec);
 extern void cost_tidscan(Path *path, PlannerInfo *root,
-						 RelOptInfo *baserel, List *tidquals, ParamPathInfo *param_info);
+						 RelOptInfo *baserel, List *tidquals,
+						 ParamPathInfo *param_info, List *filters);
 extern void cost_tidrangescan(Path *path, PlannerInfo *root,
 							  RelOptInfo *baserel, List *tidrangequals,
-							  ParamPathInfo *param_info);
+							  ParamPathInfo *param_info, List *filters);
 extern void cost_subqueryscan(SubqueryScanPath *path, PlannerInfo *root,
 							  RelOptInfo *baserel, ParamPathInfo *param_info,
 							  bool trivial_pathtarget);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index f9359f3866..7b3122a598 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -67,6 +67,7 @@ extern bool add_partial_path_precheck(RelOptInfo *parent_rel,
 
 extern bool expected_filters_equal(List *a, List *b);
 extern double expected_filters_selectivity(List *filters);
+extern double expected_filters_probes(List *filters);
 extern void apply_expected_filters(Path *path, List *filters);
 extern Path *create_filtered_scan_path(PlannerInfo *root, Path *subpath,
 									   List *filters);
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index 410f51574d..6ead27c2d8 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -9578,14 +9578,12 @@ select * from fkest f1
          Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
          ->  Seq Scan on fkest f2
                Filter: (x100 = 2)
-               Bloom Filter 1: keys=(x, x10b)
          ->  Hash
-               Bloom Filter 1
                ->  Seq Scan on fkest f1
                      Filter: (x100 = 2)
    ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
          Index Cond: (x = f1.x)
-(12 rows)
+(10 rows)
 
 alter table fkest add constraint fk
   foreign key (x, x10b, x100) references fkest (x, x10, x100);
-- 
2.43.7

