From 1f6e74d2518cd2f184309c7a9117c73d9dde4658 Mon Sep 17 00:00:00 2001
From: Rui Zhao <zhaorui126@gmail.com>
Date: Sat, 5 Sep 2026 22:57:11 +0800
Subject: [PATCH] Keep build-side alternatives realizable under exact filter
 matching

A filter is realized only by a hash join whose build side equals the
filter's build_relids exactly (compute_join_expected_filters).  Two
pieces of the candidate machinery still assume the older subset
matching, and together they can cost a scan out of its filter entirely:

- find_interesting_bloom_filters() prunes subset-related candidates
  against each other on selectivity, keeping the smaller build side
  unless the larger one discards strictly more.  That was sound under
  subset matching, where the smaller build side was realizable at every
  join the larger one was, so the more selective of the two dominated.
  Under exact matching the two are realized at different joins, and
  which of them ever appears as an inner depends on the join order
  chosen later; a less selective candidate can be the only one a given
  order can realize, and a weaker filter still beats none.  Selectivity
  alone therefore cannot establish dominance between them.  In a
  snowflake the pruning leaves the fact with only {d1} while the planner
  pre-joins the dimension subtree and the only realizable inner is
  {d1,d11,d12}.  Drop only exact duplicates; keep every candidate.

- find_bloom_filter_combinations() emits a single combination, so even
  with the candidates kept, only one of them gets a path.  Emit one
  alternative combination per filter skipped on a build-side conflict,
  built by the same greedy pass seeded with that filter, dropping
  duplicates.  Filters dropped by the bloom_filter_pushdown_max cap
  alone get no alternative: they conflict with nothing, so the primary
  combination realizes fine without them.

Each combination becomes its own path; a path expecting a filter some
join order cannot realize is rejected at that join, so the join search
keeps exactly the realizable alternative: a pre-joined dimension
subtree realizes {d1,d11,d12}, a left-deep order realizes {d1}, and an
order that joins two disjoint dimensions one at a time now applies
their two singleton filters together where before only the
cross-product build side {d1,d2} had a combination.

This restores all six filter instances the snowflake suite lost when
realization switched from subset to exact matching; the star and
simple suites are unchanged.  Two more scans take a filter from a
pre-joined build side (t6 from {t4,t5} in join.out, joinme_1 from
{foo_1,joinme,foo_2} in returning.out), two MERGE result listings
without ORDER BY change row order as the MERGE's own join plan does,
and the plan/stash advice expected outputs move to the plans the cost
model picks among the wider set of alternatives.

The price is planning time on a dimension that hangs many unrestricted
sub-dimensions: every connected subset containing it is a tied
candidate with its own path, and each such path is a join input at
every level of the search.  bloom_filter_pushdown_max_build_relids
bounds the number of such candidates.
---
 .../pg_plan_advice/expected/join_order.out    |  52 +++--
 .../expected/pg_stash_advice.out              |  98 +++++----
 src/backend/optimizer/path/allpaths.c         | 181 ++++++++++++----
 .../expected/hashjoin_bloom_snowflake.out     | 196 ++++++++++--------
 src/test/regress/expected/join.out            |  28 +--
 src/test/regress/expected/merge.out           |   4 +-
 src/test/regress/expected/returning.out       |  30 ++-
 7 files changed, 362 insertions(+), 227 deletions(-)

diff --git a/contrib/pg_plan_advice/expected/join_order.out b/contrib/pg_plan_advice/expected/join_order.out
index a274cfa4003..60a0c938e92 100644
--- a/contrib/pg_plan_advice/expected/join_order.out
+++ b/contrib/pg_plan_advice/expected/join_order.out
@@ -27,27 +27,29 @@ SELECT * FROM jo_fact f
 	LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                         QUERY PLAN                         
-------------------------------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Hash Join
-   Hash Cond: ((f.dim1_id = d1.id) AND (f.dim2_id = d2.id))
-   ->  Seq Scan on jo_fact f
-         Bloom Filter 1: keys=(dim1_id, dim2_id)
-   ->  Hash
-         Bloom Filter 1
-         ->  Nested Loop
+   Hash Cond: (f.dim2_id = d2.id)
+   ->  Hash Join
+         Hash Cond: (f.dim1_id = d1.id)
+         ->  Seq Scan on jo_fact f
+               Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
+         ->  Hash
+               Bloom Filter 1
                ->  Seq Scan on jo_dim1 d1
                      Filter: (val1 = 1)
-               ->  Materialize
-                     ->  Seq Scan on jo_dim2 d2
-                           Filter: (val2 = 1)
+   ->  Hash
+         Bloom Filter 2
+         ->  Seq Scan on jo_dim2 d2
+               Filter: (val2 = 1)
  Generated Plan Advice:
-   JOIN_ORDER(f (d1 d2))
-   NESTED_LOOP_MATERIALIZE(d2)
-   HASH_JOIN((d1 d2))
+   JOIN_ORDER(f d1 d2)
+   HASH_JOIN(d1 d2)
    SEQ_SCAN(f d1 d2)
    NO_GATHER(f d1 d2)
-(18 rows)
+(20 rows)
 
 -- Force a few different join orders. Some of these are very inefficient,
 -- but the planner considers them all viable.
@@ -58,17 +60,21 @@ SELECT * FROM jo_fact f
 	LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                QUERY PLAN                
-------------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Hash Join
    Hash Cond: (f.dim2_id = d2.id)
    ->  Hash Join
          Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on jo_fact f
+               Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
+               Bloom Filter 1
                ->  Seq Scan on jo_dim1 d1
                      Filter: (val1 = 1)
    ->  Hash
+         Bloom Filter 2
          ->  Seq Scan on jo_dim2 d2
                Filter: (val2 = 1)
  Supplied Plan Advice:
@@ -78,7 +84,7 @@ SELECT * FROM jo_fact f
    HASH_JOIN(d1 d2)
    SEQ_SCAN(f d1 d2)
    NO_GATHER(f d1 d2)
-(18 rows)
+(22 rows)
 
 SET LOCAL pg_plan_advice.advice = 'join_order(f d2 d1)';
 EXPLAIN (COSTS OFF, PLAN_ADVICE)
@@ -86,17 +92,21 @@ SELECT * FROM jo_fact f
 	LEFT JOIN jo_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN jo_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                QUERY PLAN                
-------------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Hash Join
    Hash Cond: (f.dim1_id = d1.id)
    ->  Hash Join
          Hash Cond: (f.dim2_id = d2.id)
          ->  Seq Scan on jo_fact f
+               Bloom Filter 1: keys=(dim2_id)
+               Bloom Filter 2: keys=(dim1_id)
          ->  Hash
+               Bloom Filter 1
                ->  Seq Scan on jo_dim2 d2
                      Filter: (val2 = 1)
    ->  Hash
+         Bloom Filter 2
          ->  Seq Scan on jo_dim1 d1
                Filter: (val1 = 1)
  Supplied Plan Advice:
@@ -106,7 +116,7 @@ SELECT * FROM jo_fact f
    HASH_JOIN(d2 d1)
    SEQ_SCAN(f d2 d1)
    NO_GATHER(f d1 d2)
-(18 rows)
+(22 rows)
 
 SET LOCAL pg_plan_advice.advice = 'join_order(d1 f d2)';
 EXPLAIN (COSTS OFF, PLAN_ADVICE)
diff --git a/contrib/pg_stash_advice/expected/pg_stash_advice.out b/contrib/pg_stash_advice/expected/pg_stash_advice.out
index f55f22349b0..6372bf36c2c 100644
--- a/contrib/pg_stash_advice/expected/pg_stash_advice.out
+++ b/contrib/pg_stash_advice/expected/pg_stash_advice.out
@@ -57,20 +57,24 @@ EXPLAIN (COSTS OFF)
 SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                QUERY PLAN                
-------------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Hash Join
-   Hash Cond: (f.dim1_id = d1.id)
+   Hash Cond: (f.dim2_id = d2.id)
    ->  Hash Join
-         Hash Cond: (f.dim2_id = d2.id)
+         Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
+               Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
-               ->  Seq Scan on aa_dim2 d2
-                     Filter: (val2 = 1)
+               Bloom Filter 1
+               ->  Seq Scan on aa_dim1 d1
+                     Filter: (val1 = 1)
    ->  Hash
-         ->  Seq Scan on aa_dim1 d1
-               Filter: (val1 = 1)
-(11 rows)
+         Bloom Filter 2
+         ->  Seq Scan on aa_dim2 d2
+               Filter: (val2 = 1)
+(15 rows)
 
 -- Force an index scan on dim1
 SELECT pg_set_stashed_advice('regress_stash', :'qid',
@@ -84,22 +88,26 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
 	LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                       QUERY PLAN                        
----------------------------------------------------------
+                          QUERY PLAN                           
+---------------------------------------------------------------
  Hash Join
-   Hash Cond: (f.dim1_id = d1.id)
+   Hash Cond: (f.dim2_id = d2.id)
    ->  Hash Join
-         Hash Cond: (f.dim2_id = d2.id)
+         Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
+               Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
-               ->  Seq Scan on aa_dim2 d2
-                     Filter: (val2 = 1)
+               Bloom Filter 1
+               ->  Index Scan using aa_dim1_pkey on aa_dim1 d1
+                     Filter: (val1 = 1)
    ->  Hash
-         ->  Index Scan using aa_dim1_pkey on aa_dim1 d1
-               Filter: (val1 = 1)
+         Bloom Filter 2
+         ->  Seq Scan on aa_dim2 d2
+               Filter: (val2 = 1)
  Supplied Plan Advice:
    INDEX_SCAN(d1 aa_dim1_pkey) /* matched */
-(13 rows)
+(17 rows)
 
 -- Force an alternative join order
 SELECT pg_set_stashed_advice('regress_stash', :'qid',
@@ -113,22 +121,26 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
 	LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                QUERY PLAN                
-------------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Hash Join
    Hash Cond: (f.dim2_id = d2.id)
    ->  Hash Join
          Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
+               Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
+               Bloom Filter 1
                ->  Seq Scan on aa_dim1 d1
                      Filter: (val1 = 1)
    ->  Hash
+         Bloom Filter 2
          ->  Seq Scan on aa_dim2 d2
                Filter: (val2 = 1)
  Supplied Plan Advice:
    JOIN_ORDER(f d1 d2) /* matched */
-(13 rows)
+(17 rows)
 
 -- Force an alternative join strategy
 SELECT pg_set_stashed_advice('regress_stash', :'qid',
@@ -202,20 +214,24 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
 	LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                QUERY PLAN                
-------------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Hash Join
-   Hash Cond: (f.dim1_id = d1.id)
+   Hash Cond: (f.dim2_id = d2.id)
    ->  Hash Join
-         Hash Cond: (f.dim2_id = d2.id)
+         Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
+               Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
-               ->  Seq Scan on aa_dim2 d2
-                     Filter: (val2 = 1)
+               Bloom Filter 1
+               ->  Seq Scan on aa_dim1 d1
+                     Filter: (val1 = 1)
    ->  Hash
-         ->  Seq Scan on aa_dim1 d1
-               Filter: (val1 = 1)
-(11 rows)
+         Bloom Filter 2
+         ->  Seq Scan on aa_dim2 d2
+               Filter: (val2 = 1)
+(15 rows)
 
 -- Test that we can list each stash individually and all of them together,
 -- but not a nonexistent stash.
@@ -265,20 +281,24 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
 	LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
 	LEFT JOIN aa_dim2 d2 ON f.dim2_id = d2.id
 	WHERE val1 = 1 AND val2 = 1;
-                QUERY PLAN                
-------------------------------------------
+                  QUERY PLAN                  
+----------------------------------------------
  Hash Join
-   Hash Cond: (f.dim1_id = d1.id)
+   Hash Cond: (f.dim2_id = d2.id)
    ->  Hash Join
-         Hash Cond: (f.dim2_id = d2.id)
+         Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
+               Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
-               ->  Seq Scan on aa_dim2 d2
-                     Filter: (val2 = 1)
+               Bloom Filter 1
+               ->  Seq Scan on aa_dim1 d1
+                     Filter: (val1 = 1)
    ->  Hash
-         ->  Seq Scan on aa_dim1 d1
-               Filter: (val1 = 1)
-(11 rows)
+         Bloom Filter 2
+         ->  Seq Scan on aa_dim2 d2
+               Filter: (val2 = 1)
+(15 rows)
 
 SELECT * FROM pg_get_advice_stashes() ORDER BY stash_name;
      stash_name      | num_entries 
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index 64e717d8ad4..798446c832f 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -1375,23 +1375,23 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 		}
 
 		/*
-		 * De-duplicate against the filters produced so far.  Beyond dropping
-		 * an exact duplicate build side, we keep a larger filter only when it
-		 * is strictly more selective (discards more tuples) than a smaller
-		 * one it contains: a smaller build side is sourced no later and costs
-		 * no more, so it dominates a larger build side that does not discard
-		 * more.
+		 * De-duplicate against the filters produced so far: drop only an exact
+		 * duplicate build side.
 		 *
-		 * - If an existing filter has the same build side, keep the existing
-		 * filter and drop this candidate.
-		 *
-		 * - If an existing filter's build side is a subset of this one (the
-		 * candidate is larger), keep the candidate only if it is strictly
-		 * more selective than that existing filter.
-		 *
-		 * - If an existing filter's build side is a superset of this one (the
-		 * candidate is smaller), drop that existing filter unless it is
-		 * strictly more selective than the candidate.
+		 * We deliberately do not prune between subset-related candidates on
+		 * selectivity.  Realization requires the build side to match a join's
+		 * inner side exactly (see compute_join_expected_filters), so {d1} and
+		 * {d1,d11,d12} are realized at different joins -- which of them ever
+		 * shows up as an inner depends on the join order chosen later.  A
+		 * candidate that is less selective can still be the only one a given
+		 * join order can realize, and a weaker filter beats none, so
+		 * selectivity alone can never establish that one dominates the other.
+		 * (Under the earlier subset matching the smaller build side was
+		 * realizable wherever the larger one was, which is what made pruning by
+		 * selectivity sound; exact matching removed that.)  All candidates are
+		 * kept; find_bloom_filter_combinations turns the overlapping ones into
+		 * separate combinations (and so separate paths), and the join search
+		 * keeps whichever it can realize.
 		 *
 		 * XXX Can we actually see the same set of build relids twice? I don't
 		 * think that should be possible, we only generate a single list, and
@@ -1407,21 +1407,6 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 				add = false;
 				break;
 			}
-			else if (bms_is_subset(f->build_relids, brel->relids))
-			{
-				/* existing (smaller) dominates unless candidate is better */
-				if (f->selectivity <= sel)
-				{
-					add = false;
-					break;
-				}
-			}
-			else if (bms_is_subset(brel->relids, f->build_relids))
-			{
-				/* candidate (smaller) dominates the existing larger filter */
-				if (sel <= f->selectivity)
-					result = foreach_delete_current(result, lce);
-			}
 		}
 
 		if (add)
@@ -1589,6 +1574,66 @@ expected_filter_selectivity_cmp(const ListCell *a, const ListCell *b)
 	return 0;
 }
 
+/*
+ * Greedy pass over the sorted candidates: force "seed" in first (when given),
+ * then add each filter that does not overlap the build sides already chosen,
+ * up to bloom_filter_pushdown_max members.
+ */
+static List *
+build_bloom_filter_combination(List *sorted, ExpectedFilter *seed)
+{
+	List	   *combo = NIL;
+	Relids		used = NULL;
+	ListCell   *lc;
+
+	if (seed != NULL)
+	{
+		combo = lappend(combo, seed);
+		used = bms_add_members(used, seed->build_relids);
+	}
+
+	foreach(lc, sorted)
+	{
+		ExpectedFilter *f = (ExpectedFilter *) lfirst(lc);
+
+		if (list_length(combo) >= bloom_filter_pushdown_max)
+			break;
+
+		if (f == seed)
+			continue;
+
+		/* skip a filter whose build side overlaps one already chosen */
+		if (bms_overlap(used, f->build_relids))
+			continue;
+
+		combo = lappend(combo, f);
+		used = bms_add_members(used, f->build_relids);
+	}
+
+	bms_free(used);
+
+	/* keep the most selective first; the seed may belong further down */
+	list_sort(combo, expected_filter_selectivity_cmp);
+
+	return combo;
+}
+
+/* Do two combinations apply the same set of filters? */
+static bool
+bloom_filter_combinations_equal(List *a, List *b)
+{
+	ListCell   *lc;
+
+	if (list_length(a) != list_length(b))
+		return false;
+	foreach(lc, a)
+	{
+		if (!list_member_ptr(b, lfirst(lc)))
+			return false;
+	}
+	return true;
+}
+
 /*
  * find_bloom_filter_combinations
  *	  Find the interesting Bloom filters rel could receive from a hash join
@@ -1609,8 +1654,24 @@ expected_filter_selectivity_cmp(const ListCell *a, const ListCell *b)
  * first.  bloom_filter_pushdown_max bounds how many filters are applied at
  * once, which bounds the per-tuple probe cost.
  *
- * The result is a list of combinations (each a list of ExpectedFilter); this
- * heuristic produces at most one combination.
+ * A filter skipped on a conflict is not necessarily inferior, though: a
+ * filter is realized only by a hash join whose build side matches its
+ * build_relids exactly, so of two overlapping candidates -- say {d1} and
+ * {d1,d11,d12}, equally selective -- the one that can be realized depends on
+ * the join order the planner ends up choosing.  A path expecting the wrong
+ * one is rejected at that join (see compute_join_expected_filters), and the
+ * scan would lose the filter altogether.  So for each filter skipped on a
+ * conflict we emit one alternative combination, built by the same greedy
+ * pass but with that filter forced in first.  Each combination becomes its
+ * own path, and the join search keeps whichever it can realize.  Filters
+ * dropped by the bloom_filter_pushdown_max cap alone get no alternative:
+ * they conflict with nothing, so the primary combination realizes fine
+ * without them.  Different seeds can fill up to the same set (two disjoint
+ * filters both skipped in favor of a superset covering them: either seed
+ * pulls in the other), so duplicates are dropped; this yields at most one
+ * combination per interesting filter.
+ *
+ * The result is a list of combinations (each a list of ExpectedFilter).
  *
  * Note: A CustomScan provider can call it directly from its own
  * set_rel_pathlist_hook (see generate_expected_filter_paths above).
@@ -1620,7 +1681,8 @@ find_bloom_filter_combinations(PlannerInfo *root, RelOptInfo *rel)
 {
 	List	   *filters = find_interesting_bloom_filters(root, rel);
 	List	   *sorted;
-	List	   *combo = NIL;
+	List	   *combo;
+	List	   *result;
 	Relids		used = NULL;
 	ListCell   *lc;
 
@@ -1636,28 +1698,57 @@ find_bloom_filter_combinations(PlannerInfo *root, RelOptInfo *rel)
 	sorted = list_copy(filters);
 	list_sort(sorted, expected_filter_selectivity_cmp);
 
+	/* The primary combination: plain greedy pass, no seed. */
+	combo = build_bloom_filter_combination(sorted, NULL);
+	if (combo == NIL)
+	{
+		list_free(sorted);
+		return NIL;
+	}
+
+	result = list_make1(combo);
+
+	/* Build sides covered by the primary combination. */
+	foreach(lc, combo)
+		used = bms_add_members(used,
+							   ((ExpectedFilter *) lfirst(lc))->build_relids);
+
+	/*
+	 * One alternative combination per filter skipped on a build-side
+	 * conflict, greedy-filled around that filter (see the function comment).
+	 */
 	foreach(lc, sorted)
 	{
 		ExpectedFilter *f = (ExpectedFilter *) lfirst(lc);
+		List	   *alt;
+		ListCell   *lc2;
+		bool		dup = false;
 
-		if (list_length(combo) >= bloom_filter_pushdown_max)
-			break;
-
-		/* skip a filter whose build side overlaps one already chosen */
-		if (bms_overlap(used, f->build_relids))
+		if (list_member_ptr(combo, f))
 			continue;
+		if (!bms_overlap(used, f->build_relids))
+			continue;			/* dropped by the cap alone, no conflict */
 
-		combo = lappend(combo, f);
-		used = bms_add_members(used, f->build_relids);
+		alt = build_bloom_filter_combination(sorted, f);
+
+		foreach(lc2, result)
+		{
+			if (bloom_filter_combinations_equal((List *) lfirst(lc2), alt))
+			{
+				dup = true;
+				break;
+			}
+		}
+		if (dup)
+			list_free(alt);
+		else
+			result = lappend(result, alt);
 	}
 
 	list_free(sorted);
 	bms_free(used);
 
-	if (combo == NIL)
-		return NIL;
-
-	return list_make1(combo);
+	return result;
 }
 
 /*
diff --git a/src/test/regress/expected/hashjoin_bloom_snowflake.out b/src/test/regress/expected/hashjoin_bloom_snowflake.out
index d953b247cd2..c2d627f2b7a 100644
--- a/src/test/regress/expected/hashjoin_bloom_snowflake.out
+++ b/src/test/regress/expected/hashjoin_bloom_snowflake.out
@@ -155,37 +155,41 @@ JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
 JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
 JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
 WHERE d1.r < 0.5;
-                                    QUERY PLAN                                     
------------------------------------------------------------------------------------
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
  Hash Join
-   Hash Cond: (f.id2 = d2.id)
+   Hash Cond: (f.id1 = d1.id)
    ->  Hash Join
-         Hash Cond: (f.id1 = d1.id)
+         Hash Cond: (f.id2 = d2.id)
          ->  Seq Scan on bloom_snowflake_fact f
+               Bloom Filter 3: keys=(id1)
          ->  Hash
                ->  Hash Join
-                     Hash Cond: (d12.id = d1.id12)
-                     ->  Seq Scan on bloom_snowflake_dim_1_2 d12
+                     Hash Cond: (d2.id22 = d22.id)
+                     ->  Hash Join
+                           Hash Cond: (d2.id21 = d21.id)
+                           ->  Seq Scan on bloom_snowflake_dim_2 d2
+                           ->  Hash
+                                 ->  Seq Scan on bloom_snowflake_dim_2_1 d21
                      ->  Hash
-                           ->  Hash Join
-                                 Hash Cond: (d11.id = d1.id11)
-                                 ->  Seq Scan on bloom_snowflake_dim_1_1 d11
-                                       Bloom Filter 1: keys=(id)
-                                 ->  Hash
-                                       Bloom Filter 1
-                                       ->  Seq Scan on bloom_snowflake_dim_1 d1
-                                             Filter: (r < '0.5'::double precision)
+                           ->  Seq Scan on bloom_snowflake_dim_2_2 d22
    ->  Hash
+         Bloom Filter 3
          ->  Hash Join
-               Hash Cond: (d2.id22 = d22.id)
-               ->  Hash Join
-                     Hash Cond: (d2.id21 = d21.id)
-                     ->  Seq Scan on bloom_snowflake_dim_2 d2
-                     ->  Hash
-                           ->  Seq Scan on bloom_snowflake_dim_2_1 d21
+               Hash Cond: (d12.id = d1.id12)
+               ->  Seq Scan on bloom_snowflake_dim_1_2 d12
+                     Bloom Filter 2: keys=(id)
                ->  Hash
-                     ->  Seq Scan on bloom_snowflake_dim_2_2 d22
-(28 rows)
+                     Bloom Filter 2
+                     ->  Hash Join
+                           Hash Cond: (d11.id = d1.id11)
+                           ->  Seq Scan on bloom_snowflake_dim_1_1 d11
+                                 Bloom Filter 1: keys=(id)
+                           ->  Hash
+                                 Bloom Filter 1
+                                 ->  Seq Scan on bloom_snowflake_dim_1 d1
+                                       Filter: (r < '0.5'::double precision)
+(32 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT *
@@ -197,39 +201,41 @@ JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
 JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
 JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
 WHERE d11.r < 0.5;
-                                    QUERY PLAN                                     
------------------------------------------------------------------------------------
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
  Hash Join
-   Hash Cond: (f.id2 = d2.id)
+   Hash Cond: (f.id1 = d1.id)
    ->  Hash Join
-         Hash Cond: (f.id1 = d1.id)
+         Hash Cond: (f.id2 = d2.id)
          ->  Seq Scan on bloom_snowflake_fact f
+               Bloom Filter 3: keys=(id1)
          ->  Hash
                ->  Hash Join
-                     Hash Cond: (d12.id = d1.id12)
-                     ->  Seq Scan on bloom_snowflake_dim_1_2 d12
-                           Bloom Filter 2: keys=(id)
+                     Hash Cond: (d2.id22 = d22.id)
+                     ->  Hash Join
+                           Hash Cond: (d2.id21 = d21.id)
+                           ->  Seq Scan on bloom_snowflake_dim_2 d2
+                           ->  Hash
+                                 ->  Seq Scan on bloom_snowflake_dim_2_1 d21
                      ->  Hash
-                           Bloom Filter 2
-                           ->  Hash Join
-                                 Hash Cond: (d1.id11 = d11.id)
-                                 ->  Seq Scan on bloom_snowflake_dim_1 d1
-                                       Bloom Filter 1: keys=(id11)
-                                 ->  Hash
-                                       Bloom Filter 1
-                                       ->  Seq Scan on bloom_snowflake_dim_1_1 d11
-                                             Filter: (r < '0.5'::double precision)
+                           ->  Seq Scan on bloom_snowflake_dim_2_2 d22
    ->  Hash
+         Bloom Filter 3
          ->  Hash Join
-               Hash Cond: (d2.id22 = d22.id)
-               ->  Hash Join
-                     Hash Cond: (d2.id21 = d21.id)
-                     ->  Seq Scan on bloom_snowflake_dim_2 d2
-                     ->  Hash
-                           ->  Seq Scan on bloom_snowflake_dim_2_1 d21
+               Hash Cond: (d12.id = d1.id12)
+               ->  Seq Scan on bloom_snowflake_dim_1_2 d12
+                     Bloom Filter 2: keys=(id)
                ->  Hash
-                     ->  Seq Scan on bloom_snowflake_dim_2_2 d22
-(30 rows)
+                     Bloom Filter 2
+                     ->  Hash Join
+                           Hash Cond: (d1.id11 = d11.id)
+                           ->  Seq Scan on bloom_snowflake_dim_1 d1
+                                 Bloom Filter 1: keys=(id11)
+                           ->  Hash
+                                 Bloom Filter 1
+                                 ->  Seq Scan on bloom_snowflake_dim_1_1 d11
+                                       Filter: (r < '0.5'::double precision)
+(32 rows)
 
 -- increase the accepted build size (includes the fact)
 SET bloom_filter_pushdown_max_build_relids = 4;
@@ -244,37 +250,41 @@ JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
 JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
 JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
 WHERE d1.r < 0.5;
-                                    QUERY PLAN                                     
------------------------------------------------------------------------------------
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
  Hash Join
-   Hash Cond: (f.id2 = d2.id)
+   Hash Cond: (f.id1 = d1.id)
    ->  Hash Join
-         Hash Cond: (f.id1 = d1.id)
+         Hash Cond: (f.id2 = d2.id)
          ->  Seq Scan on bloom_snowflake_fact f
+               Bloom Filter 3: keys=(id1)
          ->  Hash
                ->  Hash Join
-                     Hash Cond: (d12.id = d1.id12)
-                     ->  Seq Scan on bloom_snowflake_dim_1_2 d12
+                     Hash Cond: (d2.id22 = d22.id)
+                     ->  Hash Join
+                           Hash Cond: (d2.id21 = d21.id)
+                           ->  Seq Scan on bloom_snowflake_dim_2 d2
+                           ->  Hash
+                                 ->  Seq Scan on bloom_snowflake_dim_2_1 d21
                      ->  Hash
-                           ->  Hash Join
-                                 Hash Cond: (d11.id = d1.id11)
-                                 ->  Seq Scan on bloom_snowflake_dim_1_1 d11
-                                       Bloom Filter 1: keys=(id)
-                                 ->  Hash
-                                       Bloom Filter 1
-                                       ->  Seq Scan on bloom_snowflake_dim_1 d1
-                                             Filter: (r < '0.5'::double precision)
+                           ->  Seq Scan on bloom_snowflake_dim_2_2 d22
    ->  Hash
+         Bloom Filter 3
          ->  Hash Join
-               Hash Cond: (d2.id22 = d22.id)
-               ->  Hash Join
-                     Hash Cond: (d2.id21 = d21.id)
-                     ->  Seq Scan on bloom_snowflake_dim_2 d2
-                     ->  Hash
-                           ->  Seq Scan on bloom_snowflake_dim_2_1 d21
+               Hash Cond: (d12.id = d1.id12)
+               ->  Seq Scan on bloom_snowflake_dim_1_2 d12
+                     Bloom Filter 2: keys=(id)
                ->  Hash
-                     ->  Seq Scan on bloom_snowflake_dim_2_2 d22
-(28 rows)
+                     Bloom Filter 2
+                     ->  Hash Join
+                           Hash Cond: (d11.id = d1.id11)
+                           ->  Seq Scan on bloom_snowflake_dim_1_1 d11
+                                 Bloom Filter 1: keys=(id)
+                           ->  Hash
+                                 Bloom Filter 1
+                                 ->  Seq Scan on bloom_snowflake_dim_1 d1
+                                       Filter: (r < '0.5'::double precision)
+(32 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT *
@@ -286,39 +296,41 @@ JOIN bloom_snowflake_dim_1_2 d12 ON (d1.id12 = d12.id)
 JOIN bloom_snowflake_dim_2_1 d21 ON (d2.id21 = d21.id)
 JOIN bloom_snowflake_dim_2_2 d22 ON (d2.id22 = d22.id)
 WHERE d11.r < 0.5;
-                                    QUERY PLAN                                     
------------------------------------------------------------------------------------
+                                 QUERY PLAN                                  
+-----------------------------------------------------------------------------
  Hash Join
-   Hash Cond: (f.id2 = d2.id)
+   Hash Cond: (f.id1 = d1.id)
    ->  Hash Join
-         Hash Cond: (f.id1 = d1.id)
+         Hash Cond: (f.id2 = d2.id)
          ->  Seq Scan on bloom_snowflake_fact f
+               Bloom Filter 3: keys=(id1)
          ->  Hash
                ->  Hash Join
-                     Hash Cond: (d12.id = d1.id12)
-                     ->  Seq Scan on bloom_snowflake_dim_1_2 d12
-                           Bloom Filter 2: keys=(id)
+                     Hash Cond: (d2.id22 = d22.id)
+                     ->  Hash Join
+                           Hash Cond: (d2.id21 = d21.id)
+                           ->  Seq Scan on bloom_snowflake_dim_2 d2
+                           ->  Hash
+                                 ->  Seq Scan on bloom_snowflake_dim_2_1 d21
                      ->  Hash
-                           Bloom Filter 2
-                           ->  Hash Join
-                                 Hash Cond: (d1.id11 = d11.id)
-                                 ->  Seq Scan on bloom_snowflake_dim_1 d1
-                                       Bloom Filter 1: keys=(id11)
-                                 ->  Hash
-                                       Bloom Filter 1
-                                       ->  Seq Scan on bloom_snowflake_dim_1_1 d11
-                                             Filter: (r < '0.5'::double precision)
+                           ->  Seq Scan on bloom_snowflake_dim_2_2 d22
    ->  Hash
+         Bloom Filter 3
          ->  Hash Join
-               Hash Cond: (d2.id22 = d22.id)
-               ->  Hash Join
-                     Hash Cond: (d2.id21 = d21.id)
-                     ->  Seq Scan on bloom_snowflake_dim_2 d2
-                     ->  Hash
-                           ->  Seq Scan on bloom_snowflake_dim_2_1 d21
+               Hash Cond: (d12.id = d1.id12)
+               ->  Seq Scan on bloom_snowflake_dim_1_2 d12
+                     Bloom Filter 2: keys=(id)
                ->  Hash
-                     ->  Seq Scan on bloom_snowflake_dim_2_2 d22
-(30 rows)
+                     Bloom Filter 2
+                     ->  Hash Join
+                           Hash Cond: (d1.id11 = d11.id)
+                           ->  Seq Scan on bloom_snowflake_dim_1 d1
+                                 Bloom Filter 1: keys=(id11)
+                           ->  Hash
+                                 Bloom Filter 1
+                                 ->  Seq Scan on bloom_snowflake_dim_1_1 d11
+                                       Filter: (r < '0.5'::double precision)
+(32 rows)
 
 -- needed to stabilize the join order
 SET join_collapse_limit = 1;
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index e896a61d386..e0e8b7ff7a6 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -7610,8 +7610,10 @@ where exists (select 1 from t t4
                      Hash Cond: (t6.b = t4.b)
                      ->  Seq Scan on pg_temp.t t6
                            Output: t6.a, t6.b
+                           Bloom Filter 2: keys=(t6.b) expected=5.5%
                      ->  Hash
                            Output: t4.b, t5.b, t5.a
+                           Bloom Filter 2
                            ->  Hash Join
                                  Output: t4.b, t5.b, t5.a
                                  Inner Unique: true
@@ -7628,7 +7630,7 @@ where exists (select 1 from t t4
    ->  Index Only Scan using t_a_key on pg_temp.t t3
          Output: t3.a
          Index Cond: (t3.a = t5.a)
-(34 rows)
+(36 rows)
 
 select t1.a from t t1
   left join t t2 on t1.a = t2.a
@@ -10261,20 +10263,22 @@ select * from fkest f1
   join fkest f2 on (f1.x = f2.x and f1.x10 = f2.x10b and f1.x100 = f2.x100)
   join fkest f3 on f1.x = f3.x
   where f1.x100 = 2;
-                     QUERY PLAN                      
------------------------------------------------------
+                           QUERY PLAN                            
+-----------------------------------------------------------------
  Hash Join
-   Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
-   ->  Hash Join
-         Hash Cond: (f3.x = f2.x)
-         ->  Seq Scan on fkest f3
-         ->  Hash
+   Hash Cond: (f3.x = f1.x)
+   ->  Seq Scan on fkest f3
+         Bloom Filter 1: keys=(x)
+   ->  Hash
+         Bloom Filter 1
+         ->  Hash Join
+               Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
                ->  Seq Scan on fkest f2
                      Filter: (x100 = 2)
-   ->  Hash
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-(11 rows)
+               ->  Hash
+                     ->  Seq Scan on fkest f1
+                           Filter: (x100 = 2)
+(13 rows)
 
 rollback;
 --
diff --git a/src/test/regress/expected/merge.out b/src/test/regress/expected/merge.out
index 2fa9ba14b2a..9cb1d87066a 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -2485,8 +2485,8 @@ SELECT * FROM cj_target;
  tid | balance |               val                
 -----+---------+----------------------------------
    3 |     400 | initial source2 updated by merge
-   1 |     110 | initial source2 200
    1 |     220 | initial source2 200
+   1 |     110 | initial source2 200
    2 |     320 | initial source2 300
 (4 rows)
 
@@ -2501,8 +2501,8 @@ SELECT * FROM cj_target;
  tid | balance |               val                
 -----+---------+----------------------------------
    3 |     400 | initial source2 updated by merge
-   1 |     110 | initial source2 200
    1 |     220 | initial source2 200
+   1 |     110 | initial source2 200
    2 |     320 | initial source2 300
   10 |     100 | join input
   10 |     400 | join input
diff --git a/src/test/regress/expected/returning.out b/src/test/regress/expected/returning.out
index 93585f7f591..e2d154e77cd 100644
--- a/src/test/regress/expected/returning.out
+++ b/src/test/regress/expected/returning.out
@@ -723,30 +723,28 @@ UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57
          Hash Cond: (joinme_1.f2j = foo_1.f2)
          ->  Seq Scan on pg_temp.joinme joinme_1
                Output: joinme_1.ctid, joinme_1.f2j
+               Bloom Filter 2: keys=(joinme_1.f2j) expected=3.0%
          ->  Hash
                Output: foo_1.f2, foo_1.tableoid, foo_1.ctid, joinme.ctid, joinme.other, joinme.f2j, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid
+               Bloom Filter 2
                ->  Hash Join
                      Output: foo_1.f2, foo_1.tableoid, foo_1.ctid, joinme.ctid, joinme.other, joinme.f2j, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid
                      Hash Cond: (joinme.f2j = foo_1.f2)
-                     ->  Hash Join
-                           Output: joinme.ctid, joinme.other, joinme.f2j, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid
-                           Hash Cond: (joinme.f2j = foo_2.f2)
-                           ->  Seq Scan on pg_temp.joinme
-                                 Output: joinme.ctid, joinme.other, joinme.f2j
-                                 Bloom Filter 1: keys=(joinme.f2j) expected=0.5%
-                                 Bloom Filter 2: keys=(joinme.f2j) expected=2.0%
-                           ->  Hash
-                                 Output: foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid
-                                 Bloom Filter 1
+                     ->  Seq Scan on pg_temp.joinme
+                           Output: joinme.ctid, joinme.other, joinme.f2j
+                           Bloom Filter 1: keys=(joinme.f2j) expected=0.5%
+                     ->  Hash
+                           Output: foo_1.f2, foo_1.tableoid, foo_1.ctid, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid
+                           Bloom Filter 1
+                           ->  Nested Loop
+                                 Output: foo_1.f2, foo_1.tableoid, foo_1.ctid, foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid
+                                 Join Filter: (foo_1.f2 = foo_2.f2)
                                  ->  Seq Scan on pg_temp.foo foo_2
                                        Output: foo_2.f1, foo_2.f3, foo_2.ctid, foo_2.f2, foo_2.tableoid
                                        Filter: (foo_2.f3 = 57)
-                     ->  Hash
-                           Output: foo_1.f2, foo_1.tableoid, foo_1.ctid
-                           Bloom Filter 2
-                           ->  Seq Scan on pg_temp.foo foo_1
-                                 Output: foo_1.f2, foo_1.tableoid, foo_1.ctid
-(31 rows)
+                                 ->  Seq Scan on pg_temp.foo foo_1
+                                       Output: foo_1.f2, foo_1.tableoid, foo_1.ctid
+(29 rows)
 
 UPDATE joinview SET f3 = f3 + 1 WHERE f3 = 57
   RETURNING old.*, new.*, *, new.f3 - old.f3 AS delta_f3;
-- 
2.43.7

