From cd61a92ae7545e3018425706aded4c1fc6cf0519 Mon Sep 17 00:00:00 2001
From: test <test>
Date: Tue, 28 Jul 2026 00:08:54 +0200
Subject: [PATCH v10 12/21] Rework bloom_build_side_join_ratio

Calculate the join relation size incrementally, while building the join
relids, including considering the foreign keys etc.

Modify bloom_build_side_join_ratio to use this pre-calculated relation
size, instead of building it from scratch.

Introduces SimpleRelOptInfo, a simplified version of RelOptInfo, with
just the fields needed for generating the filter candidates. This
requires copying several internal functions that need to accept the
SimpleRelOptInto instead of the regular RelOptInfo:

- simple_calc_joinrel_size_estimate
- simple_set_joinrel_size_estimates
- simple_generate_join_implied_equalities
- simple_build_joinrel_restrictlist
- simple_subbuild_joinrel_joinlist
- simple_build_joinrel_joinlist

This code duplication is not great, but it's probably better than faking
"partial" RelOptInfo. It could also be improved in various ways, e.g. by
setting lateral_relids and using that in simple_join_is_legal etc.
---
 .../pg_plan_advice/expected/join_order.out    |   4 +-
 .../pg_plan_advice/expected/join_strategy.out |  21 +-
 contrib/pg_plan_advice/expected/semijoin.out  |  54 ++-
 src/backend/optimizer/path/allpaths.c         | 337 ++--------------
 src/backend/optimizer/path/costsize.c         | 169 ++++++++
 src/backend/optimizer/path/equivclass.c       |  92 +++++
 src/backend/optimizer/path/joinrels.c         |  87 ++++-
 src/backend/optimizer/util/relnode.c          | 164 ++++++++
 src/include/nodes/pathnodes.h                 |  33 ++
 src/include/optimizer/cost.h                  |   5 +
 src/include/optimizer/pathnode.h              |  10 +
 src/include/optimizer/paths.h                 |   5 +
 src/test/regress/expected/eager_aggregate.out |  68 ++--
 src/test/regress/expected/graph_table.out     |   8 +-
 src/test/regress/expected/join.out            | 364 +++++++++---------
 src/test/regress/expected/merge.out           | 126 +++---
 src/test/regress/expected/select_parallel.out |  37 +-
 src/test/regress/expected/stats_ext.out       |  10 +-
 src/test/regress/expected/subselect.out       |  31 +-
 src/test/regress/expected/updatable_views.out |  19 +-
 src/test/regress/expected/window.out          |  15 +-
 src/test/regress/expected/with.out            |  96 ++---
 22 files changed, 1009 insertions(+), 746 deletions(-)

diff --git a/contrib/pg_plan_advice/expected/join_order.out b/contrib/pg_plan_advice/expected/join_order.out
index 048d725e548..ab4125b4243 100644
--- a/contrib/pg_plan_advice/expected/join_order.out
+++ b/contrib/pg_plan_advice/expected/join_order.out
@@ -221,9 +221,7 @@ SELECT * FROM jo_fact f
    Hash Cond: (d2.id = f.dim2_id)
    ->  Seq Scan on jo_dim2 d2
          Filter: (val2 = 1)
-         Bloom Filter 2: keys=(id)
    ->  Hash
-         Bloom Filter 2
          ->  Hash Join
                Hash Cond: (f.dim1_id = d1.id)
                ->  Seq Scan on jo_fact f
@@ -239,7 +237,7 @@ SELECT * FROM jo_fact f
    HASH_JOIN(d1 (f d1))
    SEQ_SCAN(d2 f d1)
    NO_GATHER(f d1 d2)
-(22 rows)
+(20 rows)
 
 SET LOCAL pg_plan_advice.advice = 'join_order(d2 d1)';
 EXPLAIN (COSTS OFF, PLAN_ADVICE)
diff --git a/contrib/pg_plan_advice/expected/join_strategy.out b/contrib/pg_plan_advice/expected/join_strategy.out
index 0cb183fec80..0f9db692190 100644
--- a/contrib/pg_plan_advice/expected/join_strategy.out
+++ b/contrib/pg_plan_advice/expected/join_strategy.out
@@ -162,9 +162,7 @@ EXPLAIN (COSTS OFF, PLAN_ADVICE)
  Hash Join
    Hash Cond: (d.id = f.dim_id)
    ->  Seq Scan on join_dim d
-         Bloom Filter 1: keys=(id)
    ->  Hash
-         Bloom Filter 1
          ->  Seq Scan on join_fact f
  Supplied Plan Advice:
    HASH_JOIN(f) /* matched */
@@ -173,7 +171,7 @@ EXPLAIN (COSTS OFF, PLAN_ADVICE)
    HASH_JOIN(f)
    SEQ_SCAN(d f)
    NO_GATHER(f d)
-(14 rows)
+(12 rows)
 
 SET LOCAL pg_plan_advice.advice = 'MERGE_JOIN_MATERIALIZE(f)';
 EXPLAIN (COSTS OFF, PLAN_ADVICE)
@@ -303,22 +301,19 @@ EXPLAIN (COSTS OFF, PLAN_ADVICE)
 	SELECT * FROM join_fact f JOIN join_dim d ON f.dim_id = d.id;
                            QUERY PLAN                            
 -----------------------------------------------------------------
- Hash Join
-   Hash Cond: (d.id = f.dim_id)
-   ->  Seq Scan on join_dim d
-         Bloom Filter 1: keys=(id)
-   ->  Hash
-         Bloom Filter 1
-         ->  Seq Scan on join_fact f
+ Merge Join
+   Merge Cond: (d.id = f.dim_id)
+   ->  Index Scan using join_dim_pkey on join_dim d
+   ->  Index Scan using join_fact_dim_id on join_fact f
  Supplied Plan Advice:
    NESTED_LOOP_PLAIN(f) /* matched, conflicting, failed */
    NESTED_LOOP_MATERIALIZE(f) /* matched, conflicting, failed */
  Generated Plan Advice:
    JOIN_ORDER(d f)
-   HASH_JOIN(f)
-   SEQ_SCAN(d f)
+   MERGE_JOIN_PLAIN(f)
+   INDEX_SCAN(d public.join_dim_pkey f public.join_fact_dim_id)
    NO_GATHER(f d)
-(15 rows)
+(12 rows)
 
 SET LOCAL pg_plan_advice.advice = 'NESTED_LOOP_PLAIN(f d)';
 EXPLAIN (COSTS OFF, PLAN_ADVICE)
diff --git a/contrib/pg_plan_advice/expected/semijoin.out b/contrib/pg_plan_advice/expected/semijoin.out
index 02c5e5ec9c7..fe842294f33 100644
--- a/contrib/pg_plan_advice/expected/semijoin.out
+++ b/contrib/pg_plan_advice/expected/semijoin.out
@@ -269,9 +269,7 @@ SELECT * FROM generate_series(1,1000) g
  Hash Right Semi Join
    Hash Cond: (sj_narrow.val1 = g.g)
    ->  Seq Scan on sj_narrow
-         Bloom Filter 1: keys=(val1)
    ->  Hash
-         Bloom Filter 1
          ->  Function Scan on generate_series g
  Supplied Plan Advice:
    SEMIJOIN_NON_UNIQUE(sj_narrow) /* matched */
@@ -282,7 +280,7 @@ SELECT * FROM generate_series(1,1000) g
    SEQ_SCAN(sj_narrow)
    SEMIJOIN_NON_UNIQUE(sj_narrow)
    NO_GATHER(g sj_narrow)
-(16 rows)
+(14 rows)
 
 COMMIT;
 -- However, mentioning the wrong side of the join should result in an advice
@@ -369,21 +367,20 @@ EXPLAIN (COSTS OFF, PLAN_ADVICE)
 SELECT * FROM generate_series(1,1000) g, sj_narrow s WHERE g = s.val1;
                         QUERY PLAN                        
 ----------------------------------------------------------
- Hash Join
-   Hash Cond: (s.val1 = g.g)
-   ->  Seq Scan on sj_narrow s
-         Bloom Filter 1: keys=(val1)
-   ->  Hash
-         Bloom Filter 1
+ Merge Join
+   Merge Cond: (s.val1 = g.g)
+   ->  Index Scan using sj_narrow_val1_idx on sj_narrow s
+   ->  Sort
+         Sort Key: g.g
          ->  Function Scan on generate_series g
  Supplied Plan Advice:
    SEMIJOIN_UNIQUE(g) /* matched, inapplicable, failed */
  Generated Plan Advice:
    JOIN_ORDER(s g)
-   HASH_JOIN(g)
-   SEQ_SCAN(s)
+   MERGE_JOIN_PLAIN(g)
+   INDEX_SCAN(s public.sj_narrow_val1_idx)
    NO_GATHER(g s)
-(14 rows)
+(13 rows)
 
 COMMIT;
 -- Test the case where the subquery containing a semijoin is removed from
@@ -411,28 +408,27 @@ SELECT 1 FROM generate_series(1, 1000) g WHERE EXISTS
 	(SELECT 1 FROM
 		(SELECT 1 FROM (SELECT 1) LEFT JOIN sj_narrow ON true) s,
 		sj_narrow t2 WHERE g = t2.id);
-                                  QUERY PLAN                                  
-------------------------------------------------------------------------------
- Merge Join
-   Merge Cond: (g.g = t2.id)
-   ->  Sort
-         Sort Key: g.g
-         ->  Function Scan on generate_series g
+                               QUERY PLAN                               
+------------------------------------------------------------------------
+ Hash Join
+   Hash Cond: (t2.id = g.g)
    ->  Unique
-         ->  Nested Loop Left Join
-               ->  Nested Loop
-                     ->  Index Only Scan using sj_narrow_pkey on sj_narrow t2
-                     ->  Materialize
-                           ->  Result
+         ->  Nested Loop
+               ->  Index Only Scan using sj_narrow_pkey on sj_narrow t2
                ->  Materialize
-                     ->  Seq Scan on sj_narrow
+                     ->  Nested Loop Left Join
+                           ->  Result
+                           ->  Seq Scan on sj_narrow
+   ->  Hash
+         ->  Function Scan on generate_series g
  Generated Plan Advice:
-   JOIN_ORDER(g (t2 "*RESULT*" sj_narrow))
-   MERGE_JOIN_PLAIN((t2 sj_narrow "*RESULT*"))
-   NESTED_LOOP_MATERIALIZE("*RESULT*" sj_narrow)
+   JOIN_ORDER(t2 ("*RESULT*" sj_narrow) g)
+   NESTED_LOOP_PLAIN(sj_narrow)
+   NESTED_LOOP_MATERIALIZE((sj_narrow "*RESULT*"))
+   HASH_JOIN(g)
    SEQ_SCAN(sj_narrow)
    INDEX_ONLY_SCAN(t2 public.sj_narrow_pkey)
    SEMIJOIN_UNIQUE((t2 sj_narrow "*RESULT*"))
    NO_GATHER(g t2 sj_narrow "*RESULT*")
-(21 rows)
+(20 rows)
 
diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c
index ef96e27f56d..54335065384 100644
--- a/src/backend/optimizer/path/allpaths.c
+++ b/src/backend/optimizer/path/allpaths.c
@@ -15,6 +15,7 @@
 
 #include "postgres.h"
 
+#include <float.h>
 #include <limits.h>
 #include <math.h>
 
@@ -111,9 +112,9 @@ static void set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel,
 static List *find_interesting_bloom_filters(PlannerInfo *root,
 											RelOptInfo *rel);
 static Selectivity bloom_build_side_join_ratio(PlannerInfo *root,
+											   List *build_sides,
 											   RelOptInfo *rel,
-											   Relids build_relids,
-											   List **ec_all);
+											   Relids build_relids);
 static void generate_expected_filter_paths(PlannerInfo *root, RelOptInfo *rel);
 static void set_tablesample_rel_size(PlannerInfo *root, RelOptInfo *rel,
 									 RangeTblEntry *rte);
@@ -1058,217 +1059,29 @@ bloom_filter_recipient_reachable(PlannerInfo *root, Index owner_relid,
  * no? We should still be able to access the baserels one by one.
  */
 static Selectivity
-bloom_build_side_join_ratio(PlannerInfo *root, RelOptInfo *rel,
-							Relids build_relids, List **ec_all)
+bloom_build_side_join_ratio(PlannerInfo *root, List *build_sides,
+							RelOptInfo *rel, Relids build_relids)
 {
-	Relids		setrelids = bms_union(rel->relids, build_relids);
-	List	   *clauses = NIL;
-	List	   *ec_clauses = NIL;
-	List	   *ecs = NIL;
-	int		   *dsu;
-	double		nrows = 1.0;
-	Selectivity clausesel;
-	Selectivity fkselec;
-	SpecialJoinInfo sjinfo;
-	ListCell   *lc;
-	int			i;
-
-	/*
-	 * Product of the build relations' restriction-reduced row estimates. That
-	 * means it already reflects WHERE clauses on some of the rels (if any).
-	 */
-	i = -1;
-	while ((i = bms_next_member(build_relids, i)) >= 0)
-	{
-		RelOptInfo *br = root->simple_rel_array[i];
-
-		/* XXX paranoia */
-		Assert(br != NULL);
-
-		nrows *= br->rows;
-	}
-
-	/*
-	 * Collect candidate hashjoinable equality join clauses fully contained in
-	 * the relation set.  Ordinary (non-EC) join clauses are all applied;
-	 * EC-derived clauses are gathered separately and later reduced to a
-	 * non-redundant spanning set per EquivalenceClass.
-	 *
-	 * XXX I think this needs to be careful about using the RestrictInfo for
-	 * the reason explained in find_interesting_bloom_filters, because we
-	 * don't want to interfere with the regular selectivity estimation (by
-	 * chaching our - possibly incorrect - estimate in the RestrictInfo). So
-	 * we should strip the RestrictInfo, just like in
-	 * find_interesting_bloom_filters, or make sure the estimate is correct
-	 * (same as for regular planning). But we can strip that only after
-	 * deduplicating the EC clauses etc.
-	 */
-	i = -1;
-	while ((i = bms_next_member(setrelids, i)) >= 0)
-	{
-		RelOptInfo *r = root->simple_rel_array[i];
-		ListCell   *lc2;
-
-		/* XXX paranoia */
-		Assert(r != NULL);
-		Assert(r->reloptkind == RELOPT_BASEREL);
-
-		/*
-		 * EC-derived clauses involving this rel (cached across build sides)
-		 *
-		 * XXX This caching is imperfect, in that 'empty list' is the same as
-		 * 'not cached', so empty lists are recalculated over and over. Maybe
-		 * we should split those two concepts, and remember when we got NIL?
-		 * Unless calculating empty filter is very cheap, not sure.
-		 */
-		if (ec_all[i] == NIL)
-			ec_all[i] = generate_implied_equalities_for_all_columns(root, r,
-																	bloom_em_matches_anybarevar,
-																	NULL, NULL);
-		foreach(lc2, ec_all[i])
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc2);
-
-			if (rinfo->parent_ec == NULL)
-				continue;
-
-			if (bms_is_subset(rinfo->clause_relids, setrelids))
-			{
-				ec_clauses = list_append_unique_ptr(ec_clauses, rinfo);
-				ecs = list_append_unique_ptr(ecs, rinfo->parent_ec);
-			}
-		}
+	double	nrows = DBL_MAX;
+	Relids	relids;
 
-		/* Ordinary (non-EC) join clauses involving this rel. */
-		foreach(lc2, r->joininfo)
-		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc2);
-
-			if (rinfo->parent_ec != NULL)
-				continue;
+	ListCell *lc;
 
-			/* XXX Can this actually happen for join clauses? */
-			if (bms_membership(rinfo->clause_relids) != BMS_MULTIPLE)
-				continue;
-
-			if (bms_is_subset(rinfo->clause_relids, setrelids))
-				clauses = list_append_unique_ptr(clauses, rinfo);
-		}
-	}
+	relids = bms_copy(build_relids);
+	relids = bms_add_member(relids, rel->relid);
 
-	/*
-	 * Reduce the EC-derived clauses to a non-redundant spanning set.
-	 *
-	 * Within each EquivalenceClass, several of the collected clauses may
-	 * enforce the same equality transitively (e.g. a=b, a=c and b=c when
-	 * three relations of the set share a class); keeping them all would
-	 * multiply the same selectivity more than once.
-	 *
-	 * We use a small union-find (aka DSU) over the relids of the set, reset
-	 * per class, and keep a clause only when it connects two so-far
-	 * unconnected relations.
-	 *
-	 * The DSU tracks "parent" representative for each entry. We start with
-	 * each entry being it's own parent, and gradually merge disjoint sets
-	 * connected by the EC clauses.
-	 *
-	 * see https://en.wikipedia.org/wiki/Disjoint-set_data_structure
-	 */
-	dsu = palloc_array(int, root->simple_rel_array_size);
-	foreach(lc, ecs)
+	foreach (lc, build_sides)
 	{
-		EquivalenceClass *ec = (EquivalenceClass *) lfirst(lc);
-		ListCell   *lc2;
-
-		/* reset the DSU, each relation is a separate group */
-		for (i = 0; i < root->simple_rel_array_size; i++)
-			dsu[i] = i;
+		SimpleRelOptInfo *brel = (SimpleRelOptInfo *) lfirst(lc);
 
-		foreach(lc2, ec_clauses)
+		if (bms_equal(brel->relids, relids))
 		{
-			RestrictInfo *rinfo = (RestrictInfo *) lfirst(lc2);
-			int			first = -1;
-			bool		merged = false;
-			int			m;
-
-			/* skip clauses that don't belong to this EC */
-			if (rinfo->parent_ec != ec)
-				continue;
-
-			/* Union all set members referenced by this clause. */
-			m = -1;
-			while ((m = bms_next_member(rinfo->clause_relids, m)) >= 0)
-			{
-				int			ra,
-							rb;
-
-				Assert(m < root->simple_rel_array_size);
-
-				/* first rel in this EC clause */
-				if (first < 0)
-				{
-					first = m;
-					continue;
-				}
-
-				/* find (first) - representative of the first rel */
-				for (ra = first; dsu[ra] != ra; ra = dsu[ra])
-					 /* nothing */ ;
-
-				/* find (m) - representative of the current rel */
-				for (rb = m; dsu[rb] != rb; rb = dsu[rb])
-					 /* nothing */ ;
-
-				/* different representatives = disjoint sets, merge them */
-				if (ra != rb)
-				{
-					dsu[rb] = ra;
-					merged = true;
-				}
-			}
-
-			/*
-			 * If the clause connected two previously disconnected sets of
-			 * relations, it's not redundant. So use it for estimating filter
-			 * selectivity.
-			 */
-			if (merged)
-				clauses = lappend(clauses, rinfo);
+			nrows = brel->rows;
+			break;
 		}
 	}
-	pfree(dsu);
-
-	/* no clauses, no filtering */
-	if (clauses == NIL)
-	{
-		bms_free(setrelids);
-		return 1.0;
-	}
-
-	/*
-	 * XXX find_interesting_bloom_filters uses JOIN_SEMI, so maybe we should
-	 * use the same thing here?
-	 */
-	init_dummy_sjinfo(&sjinfo, rel->relids, build_relids);
 
-	/*
-	 * Just like calc_joinrel_size_estimate (and also
-	 * find_interesting_bloom_filters for the per-relation estimates), match
-	 * the clauses to foreign keys and estimate those using FK semantics.
-	 * Without this, joins matching a multi-column foreign key get badly
-	 * underestimated, because each of the join clauses applies the
-	 * selectivity of the restrictions on the referenced relation.
-	 */
-	fkselec = get_foreign_key_join_selectivity(root, rel->relids, build_relids,
-											   &sjinfo, &clauses);
-
-	clausesel = fkselec * clauselist_selectivity(root, clauses, 0,
-												 JOIN_INNER, &sjinfo);
-	CLAMP_PROBABILITY(clausesel);
-
-	bms_free(setrelids);
-
-	return nrows * clausesel;
+	return Min(1.0, Max(0.0, nrows / rel->rows));
 }
 
 /*
@@ -1302,13 +1115,8 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 {
 	List	   *candidates;
 	List	  **clauses_by_rel; /* clauses per build relations */
-	double	   *sel_by_rel;		/* selectivity per build relation */
-	bool	   *has_rel;		/* XXX redundant with (clauses_by_rel[r] !=
-								 * NIL) */
-	List	  **ec_all;
 	List	   *build_sides;
 	List	   *result = NIL;
-	int			i;
 	ListCell   *lc;
 
 	if (!enable_hashjoin_bloom)
@@ -1355,7 +1163,6 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 	 * referencing multiple relations? But it's fairly rare case.
 	 */
 	clauses_by_rel = palloc0_array(List *, root->simple_rel_array_size);
-	has_rel = palloc0_array(bool, root->simple_rel_array_size);
 
 	foreach(lc, candidates)
 	{
@@ -1431,67 +1238,6 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 		Assert(rinfo != NULL);
 
 		clauses_by_rel[buildrel] = lappend(clauses_by_rel[buildrel], rinfo);
-		has_rel[buildrel] = true;
-	}
-
-	/*
-	 * Pre-compute the per-build-relation semijoin selectivity.
-	 *
-	 * A build relation may be joined by multiple clauses (e.g. when the join
-	 * matches a multi-column key). Simply multiplying the per-clause
-	 * selectivities assumes the columns are independent, which is badly wrong
-	 * for such joins - it applies the selectivity of restrictions on the
-	 * build relation once per join clause. So we first match the clauses to
-	 * foreign keys (the same way join size estimates do in
-	 * calc_joinrel_size_estimate), and estimate those using FK semantics.
-	 * Only the clauses not matched to any foreign key are then estimated the
-	 * regular way.
-	 *
-	 * XXX I think we could calculate the selectivity only for semijoin-like
-	 * joins (semijoin, inner join), and ignore the rest. We don't even need
-	 * to calculate interesting filters for those cases.
-	 */
-	sel_by_rel = palloc_array(double, root->simple_rel_array_size);
-	for (i = 0; i < root->simple_rel_array_size; i++)
-	{
-		SpecialJoinInfo sjinfo;
-		Relids		build_relids;
-		List	   *worklist;
-		List	   *clauses = NIL;
-		Selectivity fkselec;
-		ListCell   *lc2;
-
-		/* ignore relations without join clauses */
-		if (!has_rel[i])
-			continue;
-
-		build_relids = bms_make_singleton(i);
-		init_dummy_sjinfo(&sjinfo, rel->relids, build_relids);
-		sjinfo.jointype = JOIN_SEMI;
-
-		/*
-		 * Estimate clauses matching a foreign key using FK semantics. This
-		 * removes the matched clauses from the worklist (which is a copy, the
-		 * original list is left alone).
-		 */
-		worklist = clauses_by_rel[i];
-		fkselec = get_foreign_key_join_selectivity(root, rel->relids,
-												   build_relids, &sjinfo,
-												   &worklist);
-
-		/* strip RestrictInfo from the remaining clauses (see comment above) */
-		foreach(lc2, worklist)
-			clauses = lappend(clauses, ((RestrictInfo *) lfirst(lc2))->clause);
-
-		sel_by_rel[i] = fkselec * clauselist_selectivity(root, clauses, 0,
-														 JOIN_SEMI, &sjinfo);
-
-		CLAMP_PROBABILITY(sel_by_rel[i]);
-
-		list_free(clauses);
-		if (worklist != clauses_by_rel[i])
-			list_free(worklist);
-		bms_free(build_relids);
 	}
 
 	/*
@@ -1518,9 +1264,6 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 	 */
 	build_sides = enumerate_bloom_filter_build_relids(root);
 
-	/* Cache of per-relation EC-derived clauses, filled lazily below. */
-	ec_all = palloc0_array(List *, root->simple_rel_array_size);
-
 	/*
 	 * Consider each legal join relation (or single base relation) that could
 	 * serve as a build side.  For a build side B we combine the per-relation
@@ -1550,15 +1293,25 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 	 */
 	foreach(lc, build_sides)
 	{
-		Relids		bset = (Relids) lfirst(lc); /* relids on build side */
+		SimpleRelOptInfo   *brel = (SimpleRelOptInfo *) lfirst(lc); /* relids on build side */
 		List	   *clauses = NIL;
-		Selectivity sel = 1.0;
+		Selectivity sel;
 		ListCell   *lce;
 		bool		add;
 		int			r;
 
 		/* The owner cannot be on the build side of the filter. */
-		if (bms_is_member(rel->relid, bset))
+		if (bms_is_member(rel->relid, brel->relids))
+			continue;
+
+		/*
+		 * Cap the build side at bloom_filter_pushdown_max_build_relids.  The
+		 * enumeration deliberately goes one relation further (so the join
+		 * ratio for a max-sized build side can be looked up), but those extra
+		 * sets exist only for that lookup and must not become filters of their
+		 * own.
+		 */
+		if (bms_num_members(brel->relids) > bloom_filter_pushdown_max_build_relids)
 			continue;
 
 		/*
@@ -1566,13 +1319,9 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 		 * the per-relation semijoin selectivities.
 		 */
 		r = -1;
-		while ((r = bms_next_member(bset, r)) >= 0)
+		while ((r = bms_next_member(brel->relids, r)) >= 0)
 		{
-			if (!has_rel[r])
-				continue;
-
 			clauses = list_concat(clauses, list_copy(clauses_by_rel[r]));
-			sel *= sel_by_rel[r];
 		}
 
 		/*
@@ -1591,16 +1340,14 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 		 * relations that have no direct join clause to the owner.  A
 		 * single-relation build side has no such internal structure, so the
 		 * per-relation semijoin estimate already covers it.
+		 *
+		 * XXX I think we could just do this for all build sides, and not
+		 * calculate the clause selectivity at all. One effect is that we
+		 * would automatically consider only filters pushed down from the
+		 * smaller to the larger relation (which is one of the heuristics
+		 * suggested by the paper anyway).
 		 */
-		if (bms_membership(bset) == BMS_MULTIPLE)
-		{
-			Selectivity join_ratio;
-
-			join_ratio = bloom_build_side_join_ratio(root, rel, bset, ec_all);
-
-			if (join_ratio < sel)
-				sel = join_ratio;
-		}
+		sel = bloom_build_side_join_ratio(root, build_sides, rel, brel->relids);
 
 		Assert((sel >= 0.0) && (sel <= 1.0));
 
@@ -1611,7 +1358,7 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 		 * de-duplication below.
 		 */
 		if (!(sel <= 1.0 - bloom_filter_pushdown_threshold && sel > 0.0) ||
-			!bloom_filter_recipient_reachable(root, rel->relid, bset))
+			!bloom_filter_recipient_reachable(root, rel->relid, brel->relids))
 		{
 			list_free(clauses);
 			continue;
@@ -1645,12 +1392,12 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 		{
 			ExpectedFilter *f = (ExpectedFilter *) lfirst(lce);
 
-			if (bms_equal(f->build_relids, bset))
+			if (bms_equal(f->build_relids, brel->relids))
 			{
 				add = false;
 				break;
 			}
-			else if (bms_is_subset(f->build_relids, bset))
+			else if (bms_is_subset(f->build_relids, brel->relids))
 			{
 				/* existing (smaller) dominates unless candidate is better */
 				if (f->selectivity <= sel)
@@ -1659,7 +1406,7 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 					break;
 				}
 			}
-			else if (bms_is_subset(bset, f->build_relids))
+			else if (bms_is_subset(brel->relids, f->build_relids))
 			{
 				/* candidate (smaller) dominates the existing larger filter */
 				if (sel <= f->selectivity)
@@ -1672,7 +1419,7 @@ find_interesting_bloom_filters(PlannerInfo *root, RelOptInfo *rel)
 			ExpectedFilter *f = makeNode(ExpectedFilter);
 
 			f->owner_relid = rel->relid;
-			f->build_relids = bms_copy(bset);
+			f->build_relids = bms_copy(brel->relids);
 			f->clauses = clauses;
 			f->selectivity = sel;
 			result = lappend(result, f);
diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c
index 0448d4f839c..85d19486c38 100644
--- a/src/backend/optimizer/path/costsize.c
+++ b/src/backend/optimizer/path/costsize.c
@@ -227,6 +227,14 @@ static double calc_joinrel_size_estimate(PlannerInfo *root,
 										 double inner_rows,
 										 SpecialJoinInfo *sjinfo,
 										 List *restrictlist);
+static double simple_calc_joinrel_size_estimate(PlannerInfo *root,
+												SimpleRelOptInfo *joinrel,
+												SimpleRelOptInfo *outer_rel,
+												SimpleRelOptInfo *inner_rel,
+												double outer_rows,
+												double inner_rows,
+												SpecialJoinInfo *sjinfo,
+												List *restrictlist);
 static Cost append_nonpartial_cost(List *subpaths, int numpaths,
 								   int parallel_workers);
 static void set_rel_width(PlannerInfo *root, RelOptInfo *rel);
@@ -5647,6 +5655,23 @@ set_joinrel_size_estimates(PlannerInfo *root, RelOptInfo *rel,
 										   restrictlist);
 }
 
+void
+simple_set_joinrel_size_estimates(PlannerInfo *root, SimpleRelOptInfo *rel,
+						   SimpleRelOptInfo *outer_rel,
+						   SimpleRelOptInfo *inner_rel,
+						   SpecialJoinInfo *sjinfo,
+						   List *restrictlist)
+{
+	rel->rows = simple_calc_joinrel_size_estimate(root,
+										   rel,
+										   outer_rel,
+										   inner_rel,
+										   outer_rel->rows,
+										   inner_rel->rows,
+										   sjinfo,
+										   restrictlist);
+}
+
 /*
  * get_parameterized_joinrel_size
  *		Make a size estimate for a parameterized scan of a join relation.
@@ -5838,6 +5863,150 @@ calc_joinrel_size_estimate(PlannerInfo *root,
 	return clamp_row_est(nrows);
 }
 
+/*
+ * calc_joinrel_size_estimate
+ *		Workhorse for set_joinrel_size_estimates and
+ *		get_parameterized_joinrel_size.
+ *
+ * outer_rel/inner_rel are the relations being joined, but they should be
+ * assumed to have sizes outer_rows/inner_rows; those numbers might be less
+ * than what rel->rows says, when we are considering parameterized paths.
+ */
+static double
+simple_calc_joinrel_size_estimate(PlannerInfo *root,
+						   SimpleRelOptInfo *joinrel,
+						   SimpleRelOptInfo *outer_rel,
+						   SimpleRelOptInfo *inner_rel,
+						   double outer_rows,
+						   double inner_rows,
+						   SpecialJoinInfo *sjinfo,
+						   List *restrictlist)
+{
+	JoinType	jointype = sjinfo->jointype;
+	Selectivity fkselec;
+	Selectivity jselec;
+	Selectivity pselec;
+	double		nrows;
+
+	/*
+	 * Compute joinclause selectivity.  Note that we are only considering
+	 * clauses that become restriction clauses at this join level; we are not
+	 * double-counting them because they were not considered in estimating the
+	 * sizes of the component rels.
+	 *
+	 * First, see whether any of the joinclauses can be matched to known FK
+	 * constraints.  If so, drop those clauses from the restrictlist, and
+	 * instead estimate their selectivity using FK semantics.  (We do this
+	 * without regard to whether said clauses are local or "pushed down".
+	 * Probably, an FK-matching clause could never be seen as pushed down at
+	 * an outer join, since it would be strict and hence would be grounds for
+	 * join strength reduction.)  fkselec gets the net selectivity for
+	 * FK-matching clauses, or 1.0 if there are none.
+	 */
+	fkselec = get_foreign_key_join_selectivity(root,
+											   outer_rel->relids,
+											   inner_rel->relids,
+											   sjinfo,
+											   &restrictlist);
+
+	/*
+	 * For an outer join, we have to distinguish the selectivity of the join's
+	 * own clauses (JOIN/ON conditions) from any clauses that were "pushed
+	 * down".  For inner joins we just count them all as joinclauses.
+	 */
+	if (IS_OUTER_JOIN(jointype))
+	{
+		List	   *joinquals = NIL;
+		List	   *pushedquals = NIL;
+		ListCell   *l;
+
+		/* Grovel through the clauses to separate into two lists */
+		foreach(l, restrictlist)
+		{
+			RestrictInfo *rinfo = lfirst_node(RestrictInfo, l);
+
+			if (RINFO_IS_PUSHED_DOWN(rinfo, joinrel->relids))
+				pushedquals = lappend(pushedquals, rinfo);
+			else
+				joinquals = lappend(joinquals, rinfo);
+		}
+
+		/* Get the separate selectivities */
+		jselec = clauselist_selectivity(root,
+										joinquals,
+										0,
+										jointype,
+										sjinfo);
+		pselec = clauselist_selectivity(root,
+										pushedquals,
+										0,
+										jointype,
+										sjinfo);
+
+		/* Avoid leaking a lot of ListCells */
+		list_free(joinquals);
+		list_free(pushedquals);
+	}
+	else
+	{
+		jselec = clauselist_selectivity(root,
+										restrictlist,
+										0,
+										jointype,
+										sjinfo);
+		pselec = 0.0;			/* not used, keep compiler quiet */
+	}
+
+	/*
+	 * Basically, we multiply size of Cartesian product by selectivity.
+	 *
+	 * If we are doing an outer join, take that into account: the joinqual
+	 * selectivity has to be clamped using the knowledge that the output must
+	 * be at least as large as the non-nullable input.  However, any
+	 * pushed-down quals are applied after the outer join, so their
+	 * selectivity applies fully.
+	 *
+	 * For JOIN_SEMI and JOIN_ANTI, the selectivity is defined as the fraction
+	 * of LHS rows that have matches, and we apply that straightforwardly.
+	 */
+	switch (jointype)
+	{
+		case JOIN_INNER:
+			nrows = outer_rows * inner_rows * fkselec * jselec;
+			/* pselec not used */
+			break;
+		case JOIN_LEFT:
+			nrows = outer_rows * inner_rows * fkselec * jselec;
+			if (nrows < outer_rows)
+				nrows = outer_rows;
+			nrows *= pselec;
+			break;
+		case JOIN_FULL:
+			nrows = outer_rows * inner_rows * fkselec * jselec;
+			if (nrows < outer_rows)
+				nrows = outer_rows;
+			if (nrows < inner_rows)
+				nrows = inner_rows;
+			nrows *= pselec;
+			break;
+		case JOIN_SEMI:
+			nrows = outer_rows * fkselec * jselec;
+			/* pselec not used */
+			break;
+		case JOIN_ANTI:
+			nrows = outer_rows * (1.0 - fkselec * jselec);
+			nrows *= pselec;
+			break;
+		default:
+			/* other values not expected here */
+			elog(ERROR, "unrecognized join type: %d", (int) jointype);
+			nrows = 0;			/* keep compiler quiet */
+			break;
+	}
+
+	return clamp_row_est(nrows);
+}
+
 /*
  * get_foreign_key_join_selectivity
  *		Estimate join selectivity for foreign-key-related clauses.
diff --git a/src/backend/optimizer/path/equivclass.c b/src/backend/optimizer/path/equivclass.c
index d1ecf6d22de..f50355dc79c 100644
--- a/src/backend/optimizer/path/equivclass.c
+++ b/src/backend/optimizer/path/equivclass.c
@@ -1645,6 +1645,98 @@ generate_join_implied_equalities(PlannerInfo *root,
 	return result;
 }
 
+List *
+simple_generate_join_implied_equalities(PlannerInfo *root,
+										Relids join_relids,
+										Relids outer_relids,
+										SimpleRelOptInfo *inner_rel,
+										SpecialJoinInfo *sjinfo)
+{
+	List	   *result = NIL;
+	Relids		inner_relids = inner_rel->relids;
+	Relids		nominal_inner_relids;
+	Relids		nominal_join_relids;
+	Bitmapset  *matching_ecs;
+	int			i;
+
+	/* If inner rel is a child, extra setup work is needed */
+//	if (IS_OTHER_REL(inner_rel))
+//	{
+//		Assert(!bms_is_empty(inner_rel->top_parent_relids));
+//
+//		/* Fetch relid set for the topmost parent rel */
+//		nominal_inner_relids = inner_rel->top_parent_relids;
+//		/* ECs will be marked with the parent's relid, not the child's */
+//		nominal_join_relids = bms_union(outer_relids, nominal_inner_relids);
+//		nominal_join_relids = add_outer_joins_to_relids(root,
+//														nominal_join_relids,
+//														sjinfo,
+//														NULL);
+//	}
+//	else
+	{
+		nominal_inner_relids = inner_relids;
+		nominal_join_relids = join_relids;
+	}
+
+	/*
+	 * Examine all potentially-relevant eclasses.
+	 *
+	 * If we are considering an outer join, we must include "join" clauses
+	 * that mention either input rel plus the outer join's relid; these
+	 * represent post-join filter clauses that have to be applied at this
+	 * join.  We don't have infrastructure that would let us identify such
+	 * eclasses cheaply, so just fall back to considering all eclasses
+	 * mentioning anything in nominal_join_relids.
+	 *
+	 * At inner joins, we can be smarter: only consider eclasses mentioning
+	 * both input rels.
+	 */
+	if (sjinfo && sjinfo->ojrelid != 0)
+		matching_ecs = get_eclass_indexes_for_relids(root, nominal_join_relids);
+	else
+		matching_ecs = get_common_eclass_indexes(root, nominal_inner_relids,
+												 outer_relids);
+
+	i = -1;
+	while ((i = bms_next_member(matching_ecs, i)) >= 0)
+	{
+		EquivalenceClass *ec = (EquivalenceClass *) list_nth(root->eq_classes, i);
+		List	   *sublist = NIL;
+
+		/* ECs containing consts do not need any further enforcement */
+		if (ec->ec_has_const)
+			continue;
+
+		/* Single-member ECs won't generate any deductions */
+		if (list_length(ec->ec_members) <= 1)
+			continue;
+
+		/* Sanity check that this eclass overlaps the join */
+		Assert(bms_overlap(ec->ec_relids, nominal_join_relids));
+
+		if (!ec->ec_broken)
+			sublist = generate_join_implied_equalities_normal(root,
+															  ec,
+															  join_relids,
+															  outer_relids,
+															  inner_relids);
+
+		/* Recover if we failed to generate required derived clauses */
+//		if (ec->ec_broken)
+//			sublist = generate_join_implied_equalities_broken(root,
+//															  ec,
+//															  nominal_join_relids,
+//															  outer_relids,
+//															  nominal_inner_relids,
+//															  inner_rel);
+
+		result = list_concat(result, sublist);
+	}
+
+	return result;
+}
+
 /*
  * generate_join_implied_equalities_for_ecs
  *	  As above, but consider only the listed ECs.
diff --git a/src/backend/optimizer/path/joinrels.c b/src/backend/optimizer/path/joinrels.c
index e0ede240822..ee8e7122ec9 100644
--- a/src/backend/optimizer/path/joinrels.c
+++ b/src/backend/optimizer/path/joinrels.c
@@ -756,6 +756,9 @@ simple_min_join_parameterization(PlannerInfo *root, Relids joinrelids,
  * realizable is later rejected by compute_join_expected_filters(), so it is
  * safe for this test to be conservative (an over-permissive or over-strict
  * answer only affects planning effort, never correctness).
+ *
+ * XXX This could/shoult be adopted to use SimpleRelOptInfo instead of the
+ * raw relids (and also the lateral_relids/direct_lateral_relids fields).
  */
 static bool
 simple_join_is_legal(PlannerInfo *root, Relids relids1, Relids relids2,
@@ -1089,6 +1092,21 @@ simple_have_relevant_joinclause(PlannerInfo *root, Relids relids1,
 	return false;
 }
 
+static SimpleRelOptInfo *
+make_simple_join_rel(Relids relids, double rows)
+{
+	SimpleRelOptInfo *rel = palloc0(sizeof(SimpleRelOptInfo));
+
+	rel->relids = relids;
+	rel->rows = rows;
+
+	rel->direct_lateral_relids = NULL;
+	rel->lateral_relids = NULL;
+	rel->joininfo = NIL;
+
+	return rel;
+}
+
 /*
  * enumerate_bloom_filter_build_relids
  *	  Enumerate the legal join relations (and single base relations) that
@@ -1164,20 +1182,29 @@ enumerate_bloom_filter_build_relids(PlannerInfo *root)
 	/*
 	 * Join levels, indexed by number of relids (starts at 1 for baserels,
 	 * same as in standard_join_search).
+	 *
+	 * We enumerate one level beyond bloom_filter_pushdown_max_build_relids:
+	 * bloom_build_side_join_ratio() estimates a build side of B relations from
+	 * the cardinality of the join {owner} + B, which has B+1 relations.  So to
+	 * get a real estimate for a build side of up to max_build_relids relations
+	 * we must have enumerated join relations of up to max_build_relids + 1.
 	 */
-	levels = palloc0_array(List *, (bloom_filter_pushdown_max_build_relids + 1));
+	levels = palloc0_array(List *, (bloom_filter_pushdown_max_build_relids + 2));
 
 	/* Level 1: each base relation on its own. */
 	levels[1] = NIL;
 	for (i = 1; i < root->simple_rel_array_size; i++)
 	{
 		RelOptInfo *rel = root->simple_rel_array[i];
+		SimpleRelOptInfo *srel;
 
 		if (rel == NULL || rel->reloptkind != RELOPT_BASEREL)
 			continue;
 
 		nbaserels++;
-		levels[1] = lappend(levels[1], bms_make_singleton(i));
+		srel = make_simple_join_rel(bms_make_singleton(i), rel->rows);
+
+		levels[1] = lappend(levels[1], srel);
 	}
 
 	/*
@@ -1201,8 +1228,13 @@ enumerate_bloom_filter_build_relids(PlannerInfo *root)
 
 	result = list_copy(levels[1]);
 
-	/* Build higher levels by adding one base relation at a time. */
-	for (level = 2; level <= bloom_filter_pushdown_max_build_relids; level++)
+	/*
+	 * Build higher levels by adding one base relation at a time.  We go one
+	 * level beyond max_build_relids so a build side of exactly that many
+	 * relations still gets a join-cardinality estimate (see the comment on the
+	 * levels array above, and bloom_build_side_join_ratio).
+	 */
+	for (level = 2; level <= bloom_filter_pushdown_max_build_relids + 1; level++)
 	{
 		ListCell   *lc;
 
@@ -1210,33 +1242,36 @@ enumerate_bloom_filter_build_relids(PlannerInfo *root)
 
 		foreach(lc, levels[level - 1])
 		{
-			Relids		oldrelids = (Relids) lfirst(lc);
+			SimpleRelOptInfo *oldrel = (SimpleRelOptInfo *) lfirst(lc);
 			ListCell   *lc2;
 
 			foreach(lc2, levels[1])
 			{
-				Relids		singleton = (Relids) lfirst(lc2);
-				int			addrel = bms_singleton_member(singleton);
+				SimpleRelOptInfo *joinrel;
+				SimpleRelOptInfo *singleton = (SimpleRelOptInfo *) lfirst(lc2);
+				int			addrel = bms_singleton_member(singleton->relids);
 				Relids		joinrelids;
 				ListCell   *lc3;
 				bool		dup;
 
 				/* The added rel must not already be part of the set. */
-				if (bms_is_member(addrel, oldrelids))
+				if (bms_is_member(addrel, oldrel->relids))
 					continue;
 
 				/* Must be connected by a join clause or order restriction. */
-				if (!simple_have_relevant_joinclause(root, oldrelids,
-													 singleton))
+				if (!simple_have_relevant_joinclause(root, oldrel->relids,
+													 singleton->relids))
 					continue;
 
-				joinrelids = bms_union(oldrelids, singleton);
+				joinrelids = bms_union(oldrel->relids, singleton->relids);
 
 				/* Skip if we already produced this set at this level. */
+				/* XXX should use a hash table with relids as a key, probably */
 				dup = false;
 				foreach(lc3, levels[level])
 				{
-					if (bms_equal((Relids) lfirst(lc3), joinrelids))
+					SimpleRelOptInfo *tmp = (SimpleRelOptInfo *) lfirst(lc3);
+					if (bms_equal(tmp->relids, joinrelids))
 					{
 						dup = true;
 						break;
@@ -1249,15 +1284,37 @@ enumerate_bloom_filter_build_relids(PlannerInfo *root)
 				}
 
 				/* Must form a legal join relation. */
-				if (!simple_join_is_legal(root, oldrelids, singleton,
+				if (!simple_join_is_legal(root, oldrel->relids, singleton->relids,
 										  joinrelids))
 				{
 					bms_free(joinrelids);
 					continue;
 				}
 
-				levels[level] = lappend(levels[level], joinrelids);
-				result = lappend(result, joinrelids);
+				joinrel = make_simple_join_rel(joinrelids, 0);
+
+				/* XXX copied from build_join_rel */
+				{
+					List *restrictlist;
+					SpecialJoinInfo sjinfo_data;
+					SpecialJoinInfo *sjinfo;
+
+					sjinfo = &sjinfo_data;
+					init_dummy_sjinfo(sjinfo, oldrel->relids, singleton->relids);
+
+					restrictlist = simple_build_joinrel_restrictlist(root, joinrel,
+																	 oldrel, singleton,
+																	 sjinfo);
+
+					simple_build_joinrel_joinlist(joinrel, oldrel, singleton);
+
+					simple_set_joinrel_size_estimates(root, joinrel,
+													  oldrel, singleton,
+													  sjinfo, restrictlist);
+				}
+
+				levels[level] = lappend(levels[level], joinrel);
+				result = lappend(result, joinrel);
 
 				/*
 				 * Stop generating joins once we hit the maximum allowed
diff --git a/src/backend/optimizer/util/relnode.c b/src/backend/optimizer/util/relnode.c
index ee69f81945f..0d6ab4639f8 100644
--- a/src/backend/optimizer/util/relnode.c
+++ b/src/backend/optimizer/util/relnode.c
@@ -71,9 +71,17 @@ static List *subbuild_joinrel_restrictlist(PlannerInfo *root,
 										   RelOptInfo *input_rel,
 										   Relids both_input_relids,
 										   List *new_restrictlist);
+static List *simple_subbuild_joinrel_restrictlist(PlannerInfo *root,
+												  SimpleRelOptInfo *joinrel,
+												  SimpleRelOptInfo *input_rel,
+												  Relids both_input_relids,
+												  List *new_restrictlist);
 static List *subbuild_joinrel_joinlist(RelOptInfo *joinrel,
 									   List *joininfo_list,
 									   List *new_joininfo);
+static List *simple_subbuild_joinrel_joinlist(SimpleRelOptInfo *joinrel,
+											  List *joininfo_list,
+											  List *new_joininfo);
 static void set_foreign_rel_properties(RelOptInfo *joinrel,
 									   RelOptInfo *outer_rel, RelOptInfo *inner_rel);
 static void add_join_rel(PlannerInfo *root, RelOptInfo *joinrel);
@@ -1502,6 +1510,43 @@ build_joinrel_restrictlist(PlannerInfo *root,
 	return result;
 }
 
+List *
+simple_build_joinrel_restrictlist(PlannerInfo *root,
+								  SimpleRelOptInfo *joinrel,
+								  SimpleRelOptInfo *outer_rel,
+								  SimpleRelOptInfo *inner_rel,
+								  SpecialJoinInfo *sjinfo)
+{
+	List	   *result;
+	Relids		both_input_relids;
+
+	both_input_relids = bms_union(outer_rel->relids, inner_rel->relids);
+
+	/*
+	 * Collect all the clauses that syntactically belong at this level,
+	 * eliminating any duplicates (important since we will see many of the
+	 * same clauses arriving from both input relations).
+	 */
+	result = simple_subbuild_joinrel_restrictlist(root, joinrel, outer_rel,
+												  both_input_relids, NIL);
+	result = simple_subbuild_joinrel_restrictlist(root, joinrel, inner_rel,
+												  both_input_relids, result);
+
+	/*
+	 * Add on any clauses derived from EquivalenceClasses.  These cannot be
+	 * redundant with the clauses in the joininfo lists, so don't bother
+	 * checking.
+	 */
+	result = list_concat(result,
+						 simple_generate_join_implied_equalities(root,
+																 joinrel->relids,
+																 outer_rel->relids,
+																 inner_rel,
+																 sjinfo));
+
+	return result;
+}
+
 static void
 build_joinrel_joinlist(RelOptInfo *joinrel,
 					   RelOptInfo *outer_rel,
@@ -1520,6 +1565,24 @@ build_joinrel_joinlist(RelOptInfo *joinrel,
 	joinrel->joininfo = result;
 }
 
+void
+simple_build_joinrel_joinlist(SimpleRelOptInfo *joinrel,
+							  SimpleRelOptInfo *outer_rel,
+							  SimpleRelOptInfo *inner_rel)
+{
+	List	   *result;
+
+	/*
+	 * Collect all the clauses that syntactically belong above this level,
+	 * eliminating any duplicates (important since we will see many of the
+	 * same clauses arriving from both input relations).
+	 */
+	result = simple_subbuild_joinrel_joinlist(joinrel, outer_rel->joininfo, NIL);
+	result = simple_subbuild_joinrel_joinlist(joinrel, inner_rel->joininfo, result);
+
+	joinrel->joininfo = result;
+}
+
 static List *
 subbuild_joinrel_restrictlist(PlannerInfo *root,
 							  RelOptInfo *joinrel,
@@ -1586,6 +1649,72 @@ subbuild_joinrel_restrictlist(PlannerInfo *root,
 	return new_restrictlist;
 }
 
+static List *
+simple_subbuild_joinrel_restrictlist(PlannerInfo *root,
+									 SimpleRelOptInfo *joinrel,
+									 SimpleRelOptInfo *input_rel,
+									 Relids both_input_relids,
+									 List *new_restrictlist)
+{
+	ListCell   *l;
+
+	foreach(l, input_rel->joininfo)
+	{
+		RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
+
+		if (bms_is_subset(rinfo->required_relids, joinrel->relids))
+		{
+			/*
+			 * This clause should become a restriction clause for the joinrel,
+			 * since it refers to no outside rels.  However, if it's a clone
+			 * clause then it might be too late to evaluate it, so we have to
+			 * check.  (If it is too late, just ignore the clause, taking it
+			 * on faith that another clone was or will be selected.)  Clone
+			 * clauses should always be outer-join clauses, so we compare
+			 * against both_input_relids.
+			 */
+			if (rinfo->has_clone || rinfo->is_clone)
+			{
+				Assert(!RINFO_IS_PUSHED_DOWN(rinfo, joinrel->relids));
+				if (!bms_is_subset(rinfo->required_relids, both_input_relids))
+					continue;
+				if (bms_overlap(rinfo->incompatible_relids, both_input_relids))
+					continue;
+			}
+			else
+			{
+				/*
+				 * For non-clone clauses, we just Assert it's OK.  These might
+				 * be either join or filter clauses; if it's a join clause
+				 * then it should not refer to the current join's output.
+				 * (There is little point in checking incompatible_relids,
+				 * because it'll be NULL.)
+				 */
+				Assert(RINFO_IS_PUSHED_DOWN(rinfo, joinrel->relids) ||
+					   bms_is_subset(rinfo->required_relids,
+									 both_input_relids));
+			}
+
+			/*
+			 * OK, so add it to the list, being careful to eliminate
+			 * duplicates.  (Since RestrictInfo nodes in different joinlists
+			 * will have been multiply-linked rather than copied, pointer
+			 * equality should be a sufficient test.)
+			 */
+			new_restrictlist = list_append_unique_ptr(new_restrictlist, rinfo);
+		}
+		else
+		{
+			/*
+			 * This clause is still a join clause at this level, so we ignore
+			 * it in this routine.
+			 */
+		}
+	}
+
+	return new_restrictlist;
+}
+
 static List *
 subbuild_joinrel_joinlist(RelOptInfo *joinrel,
 						  List *joininfo_list,
@@ -1624,6 +1753,41 @@ subbuild_joinrel_joinlist(RelOptInfo *joinrel,
 	return new_joininfo;
 }
 
+static List *
+simple_subbuild_joinrel_joinlist(SimpleRelOptInfo *joinrel,
+						  List *joininfo_list,
+						  List *new_joininfo)
+{
+	ListCell   *l;
+
+	foreach(l, joininfo_list)
+	{
+		RestrictInfo *rinfo = (RestrictInfo *) lfirst(l);
+
+		if (bms_is_subset(rinfo->required_relids, joinrel->relids))
+		{
+			/*
+			 * This clause becomes a restriction clause for the joinrel, since
+			 * it refers to no outside rels.  So we can ignore it in this
+			 * routine.
+			 */
+		}
+		else
+		{
+			/*
+			 * This clause is still a join clause at this level, so add it to
+			 * the new joininfo list, being careful to eliminate duplicates.
+			 * (Since RestrictInfo nodes in different joinlists will have been
+			 * multiply-linked rather than copied, pointer equality should be
+			 * a sufficient test.)
+			 */
+			new_joininfo = list_append_unique_ptr(new_joininfo, rinfo);
+		}
+	}
+
+	return new_joininfo;
+}
+
 
 /*
  * fetch_upper_rel
diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h
index 0a49cd36006..e17eacc3421 100644
--- a/src/include/nodes/pathnodes.h
+++ b/src/include/nodes/pathnodes.h
@@ -1236,6 +1236,39 @@ typedef struct RelOptInfo
 	int			extension_state_allocated;
 } RelOptInfo;
 
+
+/* XXX minimal subset of RelOptInfo used for filter pushdown planning */
+typedef struct SimpleRelOptInfo
+{
+	pg_node_attr(no_copy_equal, no_read, no_query_jumble)
+
+	NodeTag		type;
+
+	/*
+	 * all relations included in this RelOptInfo; set of base + OJ relids
+	 * (rangetable indexes)
+	 */
+	Relids		relids;
+
+	/*
+	 * size estimates generated by planner
+	 */
+	/* estimated number of result tuples */
+	Cardinality rows;
+
+	/*
+	 * parameterization information needed for both base rels and join rels
+	 * (see also lateral_vars and lateral_referencers)
+	 */
+	/* rels directly laterally referenced */
+	Relids		direct_lateral_relids;
+	/* minimum parameterization of rel */
+	Relids		lateral_relids;
+
+	/* RestrictInfo structures for join clauses involving this rel */
+	List	   *joininfo;
+} SimpleRelOptInfo;
+
 /*
  * Is given relation partitioned?
  *
diff --git a/src/include/optimizer/cost.h b/src/include/optimizer/cost.h
index d98eb2cb571..a313ed4e2e6 100644
--- a/src/include/optimizer/cost.h
+++ b/src/include/optimizer/cost.h
@@ -220,6 +220,11 @@ extern void set_joinrel_size_estimates(PlannerInfo *root, RelOptInfo *rel,
 									   RelOptInfo *inner_rel,
 									   SpecialJoinInfo *sjinfo,
 									   List *restrictlist);
+extern void simple_set_joinrel_size_estimates(PlannerInfo *root, SimpleRelOptInfo *rel,
+											  SimpleRelOptInfo *outer_rel,
+											  SimpleRelOptInfo *inner_rel,
+											  SpecialJoinInfo *sjinfo,
+											  List *restrictlist);
 extern void set_subquery_size_estimates(PlannerInfo *root, RelOptInfo *rel);
 extern void set_function_size_estimates(PlannerInfo *root, RelOptInfo *rel);
 extern void set_values_size_estimates(PlannerInfo *root, RelOptInfo *rel);
diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h
index 0dcae2ae3b3..f747f204184 100644
--- a/src/include/optimizer/pathnode.h
+++ b/src/include/optimizer/pathnode.h
@@ -392,4 +392,14 @@ extern RelOptInfo *build_child_join_rel(PlannerInfo *root,
 
 extern RelAggInfo *create_rel_agg_info(PlannerInfo *root, RelOptInfo *rel,
 									   bool calculate_grouped_rows);
+
+extern List *simple_build_joinrel_restrictlist(PlannerInfo *root,
+											   SimpleRelOptInfo *joinrel,
+											   SimpleRelOptInfo *outer_rel,
+											   SimpleRelOptInfo *inner_rel,
+											   SpecialJoinInfo *sjinfo);
+extern void simple_build_joinrel_joinlist(SimpleRelOptInfo *joinrel,
+										  SimpleRelOptInfo *outer_rel,
+										  SimpleRelOptInfo *inner_rel);
+
 #endif							/* PATHNODE_H */
diff --git a/src/include/optimizer/paths.h b/src/include/optimizer/paths.h
index 59801f9b0df..b153df758b3 100644
--- a/src/include/optimizer/paths.h
+++ b/src/include/optimizer/paths.h
@@ -165,6 +165,11 @@ extern List *generate_join_implied_equalities(PlannerInfo *root,
 											  Relids outer_relids,
 											  RelOptInfo *inner_rel,
 											  SpecialJoinInfo *sjinfo);
+extern List *simple_generate_join_implied_equalities(PlannerInfo *root,
+													 Relids join_relids,
+													 Relids outer_relids,
+													 SimpleRelOptInfo *inner_rel,
+													 SpecialJoinInfo *sjinfo);
 extern List *generate_join_implied_equalities_for_ecs(PlannerInfo *root,
 													  List *eclasses,
 													  Relids join_relids,
diff --git a/src/test/regress/expected/eager_aggregate.out b/src/test/regress/expected/eager_aggregate.out
index fb2cd5256c7..091ae48a92b 100644
--- a/src/test/regress/expected/eager_aggregate.out
+++ b/src/test/regress/expected/eager_aggregate.out
@@ -34,16 +34,14 @@ GROUP BY t1.a ORDER BY t1.a;
                Hash Cond: (t1.b = t2.b)
                ->  Seq Scan on public.eager_agg_t1 t1
                      Output: t1.a, t1.b, t1.c
-                     Bloom Filter 1: keys=(t1.b)
                ->  Hash
                      Output: t2.b, (PARTIAL avg(t2.c))
-                     Bloom Filter 1
                      ->  Partial HashAggregate
                            Output: t2.b, PARTIAL avg(t2.c)
                            Group Key: t2.b
                            ->  Seq Scan on public.eager_agg_t2 t2
                                  Output: t2.a, t2.b, t2.c
-(20 rows)
+(18 rows)
 
 SELECT t1.a, avg(t2.c)
   FROM eager_agg_t1 t1
@@ -82,10 +80,8 @@ GROUP BY t1.a ORDER BY t1.a;
                Hash Cond: (t1.b = t2.b)
                ->  Seq Scan on public.eager_agg_t1 t1
                      Output: t1.a, t1.b, t1.c
-                     Bloom Filter 1: keys=(t1.b)
                ->  Hash
                      Output: t2.b, (PARTIAL avg(t2.c))
-                     Bloom Filter 1
                      ->  Partial GroupAggregate
                            Output: t2.b, PARTIAL avg(t2.c)
                            Group Key: t2.b
@@ -94,7 +90,7 @@ GROUP BY t1.a ORDER BY t1.a;
                                  Sort Key: t2.b
                                  ->  Seq Scan on public.eager_agg_t2 t2
                                        Output: t2.c, t2.b
-(23 rows)
+(21 rows)
 
 SELECT t1.a, avg(t2.c)
   FROM eager_agg_t1 t1
@@ -145,16 +141,14 @@ GROUP BY t1.a ORDER BY t1.a;
                            Group Key: t2.b
                            ->  Hash Join
                                  Output: t2.c, t2.b, t3.c
-                                 Hash Cond: (t2.a = t3.a)
-                                 ->  Seq Scan on public.eager_agg_t2 t2
-                                       Output: t2.a, t2.b, t2.c
-                                       Bloom Filter 1: keys=(t2.a)
+                                 Hash Cond: (t3.a = t2.a)
+                                 ->  Seq Scan on public.eager_agg_t3 t3
+                                       Output: t3.a, t3.b, t3.c
                                  ->  Hash
-                                       Output: t3.c, t3.a
-                                       Bloom Filter 1
-                                       ->  Seq Scan on public.eager_agg_t3 t3
-                                             Output: t3.c, t3.a
-(27 rows)
+                                       Output: t2.c, t2.b, t2.a
+                                       ->  Seq Scan on public.eager_agg_t2 t2
+                                             Output: t2.c, t2.b, t2.a
+(25 rows)
 
 SELECT t1.a, avg(t2.c + t3.c)
   FROM eager_agg_t1 t1
@@ -205,16 +199,14 @@ GROUP BY t1.a ORDER BY t1.a;
                                  Sort Key: t2.b
                                  ->  Hash Join
                                        Output: t2.c, t2.b, t3.c
-                                       Hash Cond: (t2.a = t3.a)
-                                       ->  Seq Scan on public.eager_agg_t2 t2
-                                             Output: t2.a, t2.b, t2.c
-                                             Bloom Filter 1: keys=(t2.a)
+                                       Hash Cond: (t3.a = t2.a)
+                                       ->  Seq Scan on public.eager_agg_t3 t3
+                                             Output: t3.a, t3.b, t3.c
                                        ->  Hash
-                                             Output: t3.c, t3.a
-                                             Bloom Filter 1
-                                             ->  Seq Scan on public.eager_agg_t3 t3
-                                                   Output: t3.c, t3.a
-(30 rows)
+                                             Output: t2.c, t2.b, t2.a
+                                             ->  Seq Scan on public.eager_agg_t2 t2
+                                                   Output: t2.c, t2.b, t2.a
+(28 rows)
 
 SELECT t1.a, avg(t2.c + t3.c)
   FROM eager_agg_t1 t1
@@ -408,16 +400,14 @@ GROUP BY t1.a ORDER BY t1.a;
                Hash Cond: (t1.b = t2.b)
                ->  Seq Scan on public.eager_agg_t1 t1
                      Output: t1.a, t1.b, t1.c
-                     Bloom Filter 1: keys=(t1.b)
                ->  Hash
                      Output: t2.b, (PARTIAL avg(t2.c))
-                     Bloom Filter 1
                      ->  Partial HashAggregate
                            Output: t2.b, PARTIAL avg(t2.c)
                            Group Key: t2.b
                            ->  Seq Scan on public.eager_agg_t2 t2
                                  Output: t2.a, t2.b, t2.c
-(20 rows)
+(18 rows)
 
 SELECT t1.a, avg(t2.c)
   FROM eager_agg_t1 t1
@@ -452,13 +442,11 @@ GROUP BY t1.a ORDER BY t1.a;
    ->  Sort
          Sort Key: t1.a
          ->  Hash Join
-               Hash Cond: (t1.b = t2.b)
-               ->  Seq Scan on eager_agg_t1 t1
-                     Bloom Filter 1: keys=(b)
+               Hash Cond: (t2.b = t1.b)
+               ->  Seq Scan on eager_agg_t2 t2
                ->  Hash
-                     Bloom Filter 1
-                     ->  Seq Scan on eager_agg_t2 t2
-(11 rows)
+                     ->  Seq Scan on eager_agg_t1 t1
+(9 rows)
 
 EXPLAIN (COSTS OFF)
 SELECT t1.a, avg(t2.c) FILTER (WHERE random() > 0.5)
@@ -472,13 +460,11 @@ GROUP BY t1.a ORDER BY t1.a;
    ->  Sort
          Sort Key: t1.a
          ->  Hash Join
-               Hash Cond: (t1.b = t2.b)
-               ->  Seq Scan on eager_agg_t1 t1
-                     Bloom Filter 1: keys=(b)
+               Hash Cond: (t2.b = t1.b)
+               ->  Seq Scan on eager_agg_t2 t2
                ->  Hash
-                     Bloom Filter 1
-                     ->  Seq Scan on eager_agg_t2 t2
-(11 rows)
+                     ->  Seq Scan on eager_agg_t1 t1
+(9 rows)
 
 -- Eager aggregation must not push a partial aggregate onto the inner side of a
 -- SEMI or ANTI join
@@ -544,16 +530,14 @@ GROUP BY t2.b ORDER BY t2.b;
                Hash Cond: (t1.b = t2.b)
                ->  Seq Scan on public.eager_agg_t1 t1
                      Output: t1.a, t1.b, t1.c
-                     Bloom Filter 1: keys=(t1.b)
                ->  Hash
                      Output: t2.b, (PARTIAL count(*))
-                     Bloom Filter 1
                      ->  Partial HashAggregate
                            Output: t2.b, PARTIAL count(*)
                            Group Key: t2.b
                            ->  Seq Scan on public.eager_agg_t2 t2
                                  Output: t2.a, t2.b, t2.c
-(20 rows)
+(18 rows)
 
 SELECT t2.b, count(*)
   FROM eager_agg_t2 t2
diff --git a/src/test/regress/expected/graph_table.out b/src/test/regress/expected/graph_table.out
index cde3114ebf4..89643543a28 100644
--- a/src/test/regress/expected/graph_table.out
+++ b/src/test/regress/expected/graph_table.out
@@ -423,8 +423,8 @@ SELECT * FROM GRAPH_TABLE (g1 MATCH (a IS vl1 | vl2) COLUMNS (a.vname, a.vprop1)
 SELECT src, conn, dest, lprop1, vprop2, vprop1 FROM GRAPH_TABLE (g1 MATCH (a IS vl1)-[b IS el1]->(c IS vl2 | vl3) COLUMNS (a.vname AS src, b.ename AS conn, c.vname AS dest, c.lprop1, c.vprop2, c.vprop1));
  src | conn | dest |  lprop1  | vprop2 | vprop1 
 -----+------+------+----------+--------+--------
- v12 | e122 | v21  | vl2_prop |   1100 |   1010
  v11 | e121 | v22  | vl2_prop |   1200 |   1020
+ v12 | e122 | v21  | vl2_prop |   1100 |   1010
  v11 | e131 | v33  | vl3_prop |        |   2030
  v11 | e132 | v31  | vl3_prop |        |   2010
 (4 rows)
@@ -510,8 +510,8 @@ LINE 1: SELECT * FROM GRAPH_TABLE (g1 MATCH (WHERE b.eprop1 = 10001)...
 SELECT * FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (src.vname AS svname, conn.ename AS cename, dest.vname AS dvname, src.vprop1 AS svp1, src.vprop2 AS svp2, src.lprop1 AS slp1, dest.vprop1 AS dvp1, dest.vprop2 AS dvp2, dest.lprop1 AS dlp1, conn.eprop1 AS cep1, conn.lprop2 AS clp2));
  svname | cename | dvname | svp1 | svp2 |   slp1   | dvp1 | dvp2 |   dlp1   | cep1  |  clp2  
 --------+--------+--------+------+------+----------+------+------+----------+-------+--------
- v12    | e122   | v21    |   20 |      |          | 1010 | 1100 | vl2_prop | 10002 |       
  v11    | e121   | v22    |   10 |      |          | 1020 | 1200 | vl2_prop | 10001 |       
+ v12    | e122   | v21    |   20 |      |          | 1010 | 1100 | vl2_prop | 10002 |       
  v11    | e131   | v33    |   10 |      |          | 2030 |      | vl3_prop | 10003 |       
  v11    | e132   | v31    |   10 |      |          | 2010 |      | vl3_prop | 10004 |       
  v22    | e231   | v32    | 1020 | 1200 | vl2_prop | 2020 |      | vl3_prop |       | 100050
@@ -521,8 +521,8 @@ SELECT * FROM GRAPH_TABLE (g1 MATCH (src)-[conn]->(dest) COLUMNS (src.vname AS s
 SELECT * FROM GRAPH_TABLE (g1 MATCH (src IS vl1 | vl2 | vl3)-[conn]->(dest) COLUMNS (src.vname AS svname, conn.ename AS cename, dest.vname AS dvname));
  svname | cename | dvname 
 --------+--------+--------
- v12    | e122   | v21
  v11    | e121   | v22
+ v12    | e122   | v21
  v11    | e131   | v33
  v11    | e132   | v31
  v22    | e231   | v32
@@ -542,8 +542,8 @@ SELECT vn FROM all_vertices EXCEPT (SELECT svn FROM all_connected_vertices UNION
 SELECT sn, cn, dn FROM GRAPH_TABLE (g1 MATCH (src IS l1)-[conn IS l1]->(dest IS l1) COLUMNS (src.elname AS sn, conn.elname AS cn, dest.elname AS dn));
  sn  |  cn  | dn  
 -----+------+-----
- v12 | e122 | v21
  v11 | e121 | v22
+ v12 | e122 | v21
  v11 | e131 | v33
  v11 | e132 | v31
  v22 | e231 | v32
diff --git a/src/test/regress/expected/join.out b/src/test/regress/expected/join.out
index d467cba0bea..c5945f4f29a 100644
--- a/src/test/regress/expected/join.out
+++ b/src/test/regress/expected/join.out
@@ -218,13 +218,13 @@ SELECT t1.a, t2.e
   WHERE t1.a = t2.d;
  a | e  
 ---+----
+ 0 |   
  1 | -1
  2 |  2
- 3 | -3
  2 |  4
+ 3 | -3
  5 | -5
  5 | -5
- 0 |   
 (7 rows)
 
 --
@@ -1573,13 +1573,13 @@ SELECT *
   FROM J1_TBL INNER JOIN J2_TBL USING (i);
  i | j |   t   | k  
 ---+---+-------+----
+ 0 |   | zero  |   
  1 | 4 | one   | -1
  2 | 3 | two   |  2
- 3 | 2 | three | -3
  2 | 3 | two   |  4
+ 3 | 2 | three | -3
  5 | 0 | five  | -5
  5 | 0 | five  | -5
- 0 |   | zero  |   
 (7 rows)
 
 -- Same as above, slightly different syntax
@@ -1587,13 +1587,13 @@ SELECT *
   FROM J1_TBL JOIN J2_TBL USING (i);
  i | j |   t   | k  
 ---+---+-------+----
+ 0 |   | zero  |   
  1 | 4 | one   | -1
  2 | 3 | two   |  2
- 3 | 2 | three | -3
  2 | 3 | two   |  4
+ 3 | 2 | three | -3
  5 | 0 | five  | -5
  5 | 0 | five  | -5
- 0 |   | zero  |   
 (7 rows)
 
 SELECT *
@@ -1681,35 +1681,35 @@ SELECT *
   FROM J1_TBL NATURAL JOIN J2_TBL;
  i | j |   t   | k  
 ---+---+-------+----
+ 0 |   | zero  |   
  1 | 4 | one   | -1
  2 | 3 | two   |  2
- 3 | 2 | three | -3
  2 | 3 | two   |  4
+ 3 | 2 | three | -3
  5 | 0 | five  | -5
  5 | 0 | five  | -5
- 0 |   | zero  |   
 (7 rows)
 
 SELECT *
   FROM J1_TBL t1 (a, b, c) NATURAL JOIN J2_TBL t2 (a, d);
  a | b |   c   | d  
 ---+---+-------+----
+ 0 |   | zero  |   
  1 | 4 | one   | -1
  2 | 3 | two   |  2
- 3 | 2 | three | -3
  2 | 3 | two   |  4
+ 3 | 2 | three | -3
  5 | 0 | five  | -5
  5 | 0 | five  | -5
- 0 |   | zero  |   
 (7 rows)
 
 SELECT *
   FROM J1_TBL t1 (a, b, c) NATURAL JOIN J2_TBL t2 (d, a);
  a | b |  c   | d 
 ---+---+------+---
+ 0 |   | zero |  
  2 | 3 | two  | 2
  4 | 1 | four | 2
- 0 |   | zero |  
 (3 rows)
 
 -- mismatch number of columns
@@ -1718,13 +1718,13 @@ SELECT *
   FROM J1_TBL t1 (a, b) NATURAL JOIN J2_TBL t2 (a);
  a | b |   t   | k  
 ---+---+-------+----
+ 0 |   | zero  |   
  1 | 4 | one   | -1
  2 | 3 | two   |  2
- 3 | 2 | three | -3
  2 | 3 | two   |  4
+ 3 | 2 | three | -3
  5 | 0 | five  | -5
  5 | 0 | five  | -5
- 0 |   | zero  |   
 (7 rows)
 
 --
@@ -1734,22 +1734,22 @@ SELECT *
   FROM J1_TBL JOIN J2_TBL ON (J1_TBL.i = J2_TBL.i);
  i | j |   t   | i | k  
 ---+---+-------+---+----
+ 0 |   | zero  | 0 |   
  1 | 4 | one   | 1 | -1
  2 | 3 | two   | 2 |  2
- 3 | 2 | three | 3 | -3
  2 | 3 | two   | 2 |  4
+ 3 | 2 | three | 3 | -3
  5 | 0 | five  | 5 | -5
  5 | 0 | five  | 5 | -5
- 0 |   | zero  | 0 |   
 (7 rows)
 
 SELECT *
   FROM J1_TBL JOIN J2_TBL ON (J1_TBL.i = J2_TBL.k);
  i | j |  t   | i | k 
 ---+---+------+---+---
+ 0 |   | zero |   | 0
  2 | 3 | two  | 2 | 2
  4 | 1 | four | 2 | 4
- 0 |   | zero |   | 0
 (3 rows)
 
 --
@@ -3120,18 +3120,16 @@ set enable_memoize to off;
 explain (costs off)
 select count(*) from tenk1 a, tenk1 b
   where a.hundred = b.thousand and (b.fivethous % 10) < 10;
-                            QUERY PLAN                            
-------------------------------------------------------------------
+                         QUERY PLAN                         
+------------------------------------------------------------
  Aggregate
    ->  Hash Join
-         Hash Cond: (b.thousand = a.hundred)
-         ->  Seq Scan on tenk1 b
-               Filter: ((fivethous % 10) < 10)
-               Bloom Filter 1: keys=(thousand)
+         Hash Cond: (a.hundred = b.thousand)
+         ->  Index Only Scan using tenk1_hundred on tenk1 a
          ->  Hash
-               Bloom Filter 1
-               ->  Index Only Scan using tenk1_hundred on tenk1 a
-(9 rows)
+               ->  Seq Scan on tenk1 b
+                     Filter: ((fivethous % 10) < 10)
+(7 rows)
 
 select count(*) from tenk1 a, tenk1 b
   where a.hundred = b.thousand and (b.fivethous % 10) < 10;
@@ -3292,14 +3290,14 @@ select 1 from tenk1
 where (hundred, thousand) in (select twothousand, twothousand from onek);
                    QUERY PLAN                    
 -------------------------------------------------
- Hash Right Semi Join
-   Hash Cond: (onek.twothousand = tenk1.hundred)
-   ->  Seq Scan on onek
-         Bloom Filter 1: keys=(twothousand)
+ Hash Join
+   Hash Cond: (tenk1.hundred = onek.twothousand)
+   ->  Seq Scan on tenk1
+         Filter: (hundred = thousand)
    ->  Hash
-         Bloom Filter 1
-         ->  Seq Scan on tenk1
-               Filter: (hundred = thousand)
+         ->  HashAggregate
+               Group Key: onek.twothousand
+               ->  Seq Scan on onek
 (8 rows)
 
 reset enable_memoize;
@@ -3375,16 +3373,17 @@ where t3.c is null;
 -------------------------------------------
  Hash Right Anti Join
    Hash Cond: (t2.b = t1.unique1)
-   ->  Hash Join
-         Hash Cond: (t2.c = t3.c)
-         ->  Seq Scan on tbl_anti t2
-               Bloom Filter 1: keys=(c)
-         ->  Hash
-               Bloom Filter 1
+   ->  Merge Join
+         Merge Cond: (t2.c = t3.c)
+         ->  Sort
+               Sort Key: t2.c
+               ->  Seq Scan on tbl_anti t2
+         ->  Sort
+               Sort Key: t3.c
                ->  Seq Scan on tbl_anti t3
    ->  Hash
          ->  Seq Scan on tenk1 t1
-(11 rows)
+(12 rows)
 
 -- this is an antijoin: the strict join clause t2.c = t3.c guarantees t2.c is
 -- non-null
@@ -3397,20 +3396,20 @@ where t2.c is null;
 -------------------------------------------------------
  Hash Right Anti Join
    Hash Cond: (t2.b = t1.unique1)
-   ->  Merge Join
-         Merge Cond: (t2.c = t3.c)
+   ->  Merge Right Join
+         Merge Cond: (t4.c = t3.c)
          ->  Sort
-               Sort Key: t2.c
-               ->  Seq Scan on tbl_anti t2
+               Sort Key: t4.c
+               ->  Seq Scan on tbl_anti t4
          ->  Materialize
-               ->  Merge Left Join
-                     Merge Cond: (t3.c = t4.c)
+               ->  Merge Join
+                     Merge Cond: (t2.c = t3.c)
+                     ->  Sort
+                           Sort Key: t2.c
+                           ->  Seq Scan on tbl_anti t2
                      ->  Sort
                            Sort Key: t3.c
                            ->  Seq Scan on tbl_anti t3
-                     ->  Sort
-                           Sort Key: t4.c
-                           ->  Seq Scan on tbl_anti t4
    ->  Hash
          ->  Seq Scan on tenk1 t1
 (18 rows)
@@ -3426,16 +3425,17 @@ where ss.c is null;
 -------------------------------------------
  Hash Right Anti Join
    Hash Cond: (t2.b = t1.unique1)
-   ->  Hash Join
-         Hash Cond: (t2.c = t3.c)
-         ->  Seq Scan on tbl_anti t2
-               Bloom Filter 1: keys=(c)
-         ->  Hash
-               Bloom Filter 1
+   ->  Merge Join
+         Merge Cond: (t2.c = t3.c)
+         ->  Sort
+               Sort Key: t2.c
+               ->  Seq Scan on tbl_anti t2
+         ->  Sort
+               Sort Key: t3.c
                ->  Seq Scan on tbl_anti t3
    ->  Hash
          ->  Seq Scan on tenk1 t1
-(11 rows)
+(12 rows)
 
 -- this is an antijoin: the strict join clause t2.c = t3.c guarantees t2.c is
 -- non-null
@@ -3452,13 +3452,11 @@ where ss.c is null;
          ->  Hash Join
                Hash Cond: (t2.c = t3.c)
                ->  Seq Scan on tbl_anti t2
-                     Bloom Filter 1: keys=(c)
                ->  Hash
-                     Bloom Filter 1
                      ->  HashAggregate
                            Group Key: t3.c
                            ->  Seq Scan on tbl_anti t3
-(12 rows)
+(10 rows)
 
 -- this is not an antijoin: the join clause t2.c = t3.c cannot guarantee t3.c
 -- is non-null
@@ -3493,16 +3491,17 @@ where t3.c is null;
  Hash Right Join
    Hash Cond: (t2.b = t1.unique1)
    Filter: (t3.c IS NULL)
-   ->  Hash Join
-         Hash Cond: (t2.c = t3.c)
-         ->  Seq Scan on tbl_anti t2
-               Bloom Filter 1: keys=(c)
-         ->  Hash
-               Bloom Filter 1
+   ->  Merge Join
+         Merge Cond: (t2.c = t3.c)
+         ->  Sort
+               Sort Key: t2.c
+               ->  Seq Scan on tbl_anti t2
+         ->  Sort
+               Sort Key: t3.c
                ->  Seq Scan on tbl_anti t3
    ->  Hash
          ->  Seq Scan on tenk1 t1
-(12 rows)
+(13 rows)
 
 -- likewise with full join
 explain (costs off)
@@ -3537,17 +3536,18 @@ where t1.b is null;
 -------------------------------------------
  Hash Right Join
    Hash Cond: (t3.b = t1.b)
-   ->  Hash Join
-         Hash Cond: (t2.c = t3.c)
-         ->  Seq Scan on tbl_anti t2
-               Bloom Filter 1: keys=(c)
-         ->  Hash
-               Bloom Filter 1
+   ->  Merge Join
+         Merge Cond: (t2.c = t3.c)
+         ->  Sort
+               Sort Key: t2.c
+               ->  Seq Scan on tbl_anti t2
+         ->  Sort
+               Sort Key: t3.c
                ->  Seq Scan on tbl_anti t3
    ->  Hash
          ->  Seq Scan on tbl_anti t1
                Filter: (b IS NULL)
-(12 rows)
+(13 rows)
 
 -- a full join can also reduce to an antijoin: the forced-null Var is defined
 -- not null
@@ -3588,14 +3588,15 @@ where t3.c is null;
    Hash Cond: (t1.b = t2.b)
    ->  Seq Scan on tbl_anti t1
    ->  Hash
-         ->  Hash Join
-               Hash Cond: (t2.c = t3.c)
-               ->  Seq Scan on tbl_anti t2
-                     Bloom Filter 1: keys=(c)
-               ->  Hash
-                     Bloom Filter 1
+         ->  Merge Join
+               Merge Cond: (t2.c = t3.c)
+               ->  Sort
+                     Sort Key: t2.c
+                     ->  Seq Scan on tbl_anti t2
+               ->  Sort
+                     Sort Key: t3.c
                      ->  Seq Scan on tbl_anti t3
-(11 rows)
+(12 rows)
 
 -- the symmetric case, with the forced-null Var on the left side
 explain (costs off)
@@ -3608,14 +3609,15 @@ where t1.c is null;
    Hash Cond: (t2.b = t1.b)
    ->  Seq Scan on tbl_anti t2
    ->  Hash
-         ->  Hash Join
-               Hash Cond: (t1.c = t4.c)
-               ->  Seq Scan on tbl_anti t1
-                     Bloom Filter 1: keys=(c)
-               ->  Hash
-                     Bloom Filter 1
+         ->  Merge Join
+               Merge Cond: (t1.c = t4.c)
+               ->  Sort
+                     Sort Key: t1.c
+                     ->  Seq Scan on tbl_anti t1
+               ->  Sort
+                     Sort Key: t4.c
                      ->  Seq Scan on tbl_anti t4
-(11 rows)
+(12 rows)
 
 -- this is not an antijoin: the join clause cannot prove t2.b non-null in the
 -- full join's right-only rows
@@ -3714,15 +3716,16 @@ where t2.a is null;
    Hash Cond: (t1.b = t2.b)
    ->  Seq Scan on tbl_anti t1
    ->  Hash
-         ->  Hash Join
-               Hash Cond: (t2.c = t3.c)
-               ->  Seq Scan on tbl_anti t2
-                     Bloom Filter 1: keys=(c)
-               ->  Hash
-                     Bloom Filter 1
+         ->  Merge Join
+               Merge Cond: (t3.c = t2.c)
+               ->  Sort
+                     Sort Key: t3.c
                      ->  Seq Scan on tbl_anti t3
                            Filter: (c > 0)
-(12 rows)
+               ->  Sort
+                     Sort Key: t2.c
+                     ->  Seq Scan on tbl_anti t2
+(13 rows)
 
 -- A USING join column becomes a merged (COALESCE) output, so reducing the
 -- full join also has to fix up its nulling rels.
@@ -3822,14 +3825,15 @@ where t2 is null;
  Nested Loop Anti Join
    ->  Seq Scan on tenk1 t1
    ->  Materialize
-         ->  Hash Join
-               Hash Cond: (t2.c = t3.c)
-               ->  Seq Scan on tbl_wr t2
-                     Bloom Filter 1: keys=(c)
-               ->  Hash
-                     Bloom Filter 1
+         ->  Merge Join
+               Merge Cond: (t3.c = t2.c)
+               ->  Sort
+                     Sort Key: t3.c
                      ->  Seq Scan on tbl_anti t3
-(10 rows)
+               ->  Sort
+                     Sort Key: t2.c
+                     ->  Seq Scan on tbl_wr t2
+(11 rows)
 
 -- this is not an antijoin: nothing proves any column of t2 non-null
 explain (costs off)
@@ -3950,11 +3954,9 @@ where t2 is not null;
    Hash Cond: (t2.b = t1.unique1)
    ->  Seq Scan on tbl_wr t2
          Filter: (t2.* IS NOT NULL)
-         Bloom Filter 1: keys=(b)
    ->  Hash
-         Bloom Filter 1
          ->  Seq Scan on tenk1 t1
-(8 rows)
+(6 rows)
 
 -- reduced to a left join
 explain (costs off)
@@ -3984,11 +3986,9 @@ where t2.w is not null;
    Hash Cond: (t2.a = t1.unique1)
    ->  Seq Scan on tbl_comp t2
          Filter: (w IS NOT NULL)
-         Bloom Filter 1: keys=(a)
    ->  Hash
-         Bloom Filter 1
          ->  Seq Scan on tenk1 t1
-(8 rows)
+(6 rows)
 
 -- composite-type columns: reduced to an antijoin
 explain (costs off)
@@ -4215,16 +4215,14 @@ create temp table tidv (idv mycomptype);
 create index on tidv (idv);
 explain (costs off)
 select a.idv, b.idv from tidv a, tidv b where a.idv = b.idv;
-             QUERY PLAN             
-------------------------------------
- Hash Join
-   Hash Cond: (a.idv = b.idv)
-   ->  Seq Scan on tidv a
-         Bloom Filter 1: keys=(idv)
-   ->  Hash
-         Bloom Filter 1
-         ->  Seq Scan on tidv b
-(7 rows)
+                        QUERY PLAN                        
+----------------------------------------------------------
+ Merge Join
+   Merge Cond: (a.idv = b.idv)
+   ->  Index Only Scan using tidv_idv_idx on tidv a
+   ->  Materialize
+         ->  Index Only Scan using tidv_idv_idx on tidv b
+(5 rows)
 
 set enable_mergejoin = 0;
 set enable_hashjoin = 0;
@@ -4777,43 +4775,43 @@ select ss1.d1 from
     on i8.q1 = i4.f1
   on t1.tenthous = ss1.d1
 where t1.unique1 < i4.f1;
-                                                                                                             QUERY PLAN                                                                                                              
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
- Hash Join
+                                                                                                          QUERY PLAN                                                                                                           
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Nested Loop
    Output: ((64)::information_schema.cardinal_number)
-   Hash Cond: (t2.ten = t1.tenthous)
-   ->  Seq Scan on public.tenk1 t2
-         Output: t2.unique1, t2.unique2, t2.two, t2.four, t2.ten, t2.twenty, t2.hundred, t2.thousand, t2.twothousand, t2.fivethous, t2.tenthous, t2.odd, t2.even, t2.stringu1, t2.stringu2, t2.string4
-         Bloom Filter 1: keys=(t2.ten)
-   ->  Hash
-         Output: t1.tenthous, ((64)::information_schema.cardinal_number)
-         Bloom Filter 1
-         ->  Nested Loop
-               Output: t1.tenthous, (64)::information_schema.cardinal_number
-               Join Filter: (t1.tenthous = ((64)::information_schema.cardinal_number)::integer)
-               ->  Seq Scan on public.tenk1 t3
-                     Output: t3.unique1, t3.unique2, t3.two, t3.four, t3.ten, t3.twenty, t3.hundred, t3.thousand, t3.twothousand, t3.fivethous, t3.tenthous, t3.odd, t3.even, t3.stringu1, t3.stringu2, t3.string4
-                     Filter: (t3.fivethous < 0)
+   Join Filter: (t1.unique1 < i4.f1)
+   ->  Nested Loop
+         Output: i4.f1
+         ->  Seq Scan on public.int8_tbl i8
+               Output: i8.q1, i8.q2
+               Filter: (i8.q1 = ((64)::information_schema.cardinal_number)::integer)
+         ->  Seq Scan on public.int4_tbl i4
+               Output: i4.f1
+               Filter: (i4.f1 = ((64)::information_schema.cardinal_number)::integer)
+   ->  Hash Join
+         Output: t1.unique1, ((64)::information_schema.cardinal_number)
+         Hash Cond: (t2.ten = t1.tenthous)
+         ->  Seq Scan on public.tenk1 t2
+               Output: t2.unique1, t2.unique2, t2.two, t2.four, t2.ten, t2.twenty, t2.hundred, t2.thousand, t2.twothousand, t2.fivethous, t2.tenthous, t2.odd, t2.even, t2.stringu1, t2.stringu2, t2.string4
+               Bloom Filter 1: keys=(t2.ten) expected=0.0%
+         ->  Hash
+               Output: t1.tenthous, t1.unique1, ((64)::information_schema.cardinal_number)
+               Bloom Filter 1
                ->  Nested Loop
-                     Output: t1.tenthous
+                     Output: t1.tenthous, t1.unique1, (64)::information_schema.cardinal_number
+                     Join Filter: (t1.tenthous = ((64)::information_schema.cardinal_number)::integer)
+                     ->  Seq Scan on public.tenk1 t3
+                           Output: t3.unique1, t3.unique2, t3.two, t3.four, t3.ten, t3.twenty, t3.hundred, t3.thousand, t3.twothousand, t3.fivethous, t3.tenthous, t3.odd, t3.even, t3.stringu1, t3.stringu2, t3.string4
+                           Filter: (t3.fivethous < 0)
                      ->  Nested Loop
-                           Output: t1.tenthous, i4.f1
-                           Join Filter: (t1.unique1 < i4.f1)
-                           ->  Nested Loop
-                                 Output: t1.tenthous, t1.unique1
-                                 ->  Subquery Scan on ss0
-                                       Output: ss0.x, (64)::information_schema.cardinal_number
-                                       ->  Result
-                                             Output: ((abs(t3.unique1))::double precision + random())
-                                 ->  Index Scan using tenk1_thous_tenthous on public.tenk1 t1
-                                       Output: t1.unique1, t1.unique2, t1.two, t1.four, t1.ten, t1.twenty, t1.hundred, t1.thousand, t1.twothousand, t1.fivethous, t1.tenthous, t1.odd, t1.even, t1.stringu1, t1.stringu2, t1.string4
-                                       Index Cond: (t1.tenthous = (((64)::information_schema.cardinal_number))::integer)
-                           ->  Seq Scan on public.int4_tbl i4
-                                 Output: i4.f1
-                                 Filter: (i4.f1 = ((64)::information_schema.cardinal_number)::integer)
-                     ->  Seq Scan on public.int8_tbl i8
-                           Output: i8.q1, i8.q2
-                           Filter: (i8.q1 = ((64)::information_schema.cardinal_number)::integer)
+                           Output: t1.tenthous, t1.unique1
+                           ->  Subquery Scan on ss0
+                                 Output: ss0.x, (64)::information_schema.cardinal_number
+                                 ->  Result
+                                       Output: ((abs(t3.unique1))::double precision + random())
+                           ->  Index Scan using tenk1_thous_tenthous on public.tenk1 t1
+                                 Output: t1.unique1, t1.unique2, t1.two, t1.four, t1.ten, t1.twenty, t1.hundred, t1.thousand, t1.twothousand, t1.fivethous, t1.tenthous, t1.odd, t1.even, t1.stringu1, t1.stringu2, t1.string4
+                                 Index Cond: (t1.tenthous = (((64)::information_schema.cardinal_number))::integer)
 (35 rows)
 
 select ss1.d1 from
@@ -8927,32 +8925,33 @@ JOIN (
 		)
 	) _t2t3t4
 ON sj_t1.id = _t2t3t4.id;
-                                        QUERY PLAN                                         
--------------------------------------------------------------------------------------------
+                                     QUERY PLAN                                      
+-------------------------------------------------------------------------------------
  Nested Loop
    Join Filter: (sj_t1.id = sj_t3.id)
    ->  Nested Loop
-         Join Filter: (sj_t2_1.id = sj_t3.id)
-         ->  Nested Loop
-               Join Filter: (sj_t2.id = sj_t3.id)
+         Join Filter: (sj_t3.id = sj_t2.id)
+         ->  Nested Loop Semi Join
                ->  Nested Loop
-                     ->  Unique
-                           ->  Nested Loop
-                                 ->  Index Only Scan using sj_t3_a_id_idx on sj_t3 sj_t3_1
-                                       Index Cond: (a = 1)
-                                 ->  Seq Scan on sj_t4 sj_t4_1
-                     ->  Index Only Scan using sj_t2_id_idx on sj_t2
-                           Index Cond: (id = sj_t3_1.id)
-               ->  Materialize
-                     ->  Unique
+                     ->  HashAggregate
+                           Group Key: sj_t3.id
                            ->  Nested Loop
-                                 ->  Index Only Scan using sj_t3_a_id_idx on sj_t3
-                                       Index Cond: (a = 1)
                                  ->  Seq Scan on sj_t4
-         ->  Index Only Scan using sj_t2_id_idx on sj_t2 sj_t2_1
-               Index Cond: (id = sj_t2.id)
+                                 ->  Materialize
+                                       ->  Bitmap Heap Scan on sj_t3
+                                             Recheck Cond: (a = 1)
+                                             ->  Bitmap Index Scan on sj_t3_a_id_idx
+                                                   Index Cond: (a = 1)
+                     ->  Index Only Scan using sj_t2_id_idx on sj_t2 sj_t2_1
+                           Index Cond: (id = sj_t3.id)
+               ->  Nested Loop
+                     ->  Index Only Scan using sj_t3_a_id_idx on sj_t3 sj_t3_1
+                           Index Cond: ((a = 1) AND (id = sj_t3.id))
+                     ->  Seq Scan on sj_t4 sj_t4_1
+         ->  Index Only Scan using sj_t2_id_idx on sj_t2
+               Index Cond: (id = sj_t2_1.id)
    ->  Seq Scan on sj_t1
-(23 rows)
+(24 rows)
 
 --
 -- Test RowMarks-related code
@@ -10264,21 +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: ((f1.x = f2.x) AND (f1.x10 = f2.x10b))
-   ->  Nested Loop
-         ->  Seq Scan on fkest f1
-               Filter: (x100 = 2)
-               Bloom Filter 1: keys=(x, x10)
-         ->  Index Scan using fkest_x_x10_x100_idx on fkest f3
-               Index Cond: (x = f1.x)
+   Hash Cond: ((f2.x = f1.x) AND (f2.x10b = f1.x10))
+   ->  Seq Scan on fkest f2
+         Filter: (x100 = 2)
    ->  Hash
-         Bloom Filter 1
-         ->  Seq Scan on fkest f2
-               Filter: (x100 = 2)
-(12 rows)
+         ->  Hash Join
+               Hash Cond: (f3.x = f1.x)
+               ->  Seq Scan on fkest f3
+                     Bloom Filter 1: keys=(x)
+               ->  Hash
+                     Bloom Filter 1
+                     ->  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 139aba47824..2fa9ba14b2a 100644
--- a/src/test/regress/expected/merge.out
+++ b/src/test/regress/expected/merge.out
@@ -39,17 +39,18 @@ USING source AS s
 ON t.tid = s.sid
 WHEN MATCHED THEN
 	DELETE;
-                QUERY PLAN                
-------------------------------------------
+               QUERY PLAN               
+----------------------------------------
  Merge on target t
-   ->  Hash Join
-         Hash Cond: (t.tid = s.sid)
-         ->  Seq Scan on target t
-               Bloom Filter 1: keys=(tid)
-         ->  Hash
-               Bloom Filter 1
+   ->  Merge Join
+         Merge Cond: (t.tid = s.sid)
+         ->  Sort
+               Sort Key: t.tid
+               ->  Seq Scan on target t
+         ->  Sort
+               Sort Key: s.sid
                ->  Seq Scan on source s
-(8 rows)
+(9 rows)
 
 --
 -- Errors
@@ -322,17 +323,15 @@ USING source AS s
 ON t.tid = s.sid
 WHEN MATCHED THEN
 	UPDATE SET balance = 0;
-                QUERY PLAN                
-------------------------------------------
+               QUERY PLAN               
+----------------------------------------
  Merge on target t
    ->  Hash Join
          Hash Cond: (s.sid = t.tid)
          ->  Seq Scan on source s
-               Bloom Filter 1: keys=(sid)
          ->  Hash
-               Bloom Filter 1
                ->  Seq Scan on target t
-(8 rows)
+(6 rows)
 
 EXPLAIN (COSTS OFF)
 MERGE INTO target t
@@ -340,17 +339,15 @@ USING source AS s
 ON t.tid = s.sid
 WHEN MATCHED THEN
 	DELETE;
-                QUERY PLAN                
-------------------------------------------
+               QUERY PLAN               
+----------------------------------------
  Merge on target t
    ->  Hash Join
          Hash Cond: (s.sid = t.tid)
          ->  Seq Scan on source s
-               Bloom Filter 1: keys=(sid)
          ->  Hash
-               Bloom Filter 1
                ->  Seq Scan on target t
-(8 rows)
+(6 rows)
 
 EXPLAIN (COSTS OFF)
 MERGE INTO target t
@@ -1639,38 +1636,42 @@ SELECT explain_merge('
 MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
 WHEN MATCHED THEN
 	UPDATE SET b = t.b + 1');
-                                      explain_merge                                       
-------------------------------------------------------------------------------------------
+                              explain_merge                              
+-------------------------------------------------------------------------
  Merge on ex_mtarget t (actual rows=0.00 loops=1)
    Tuples: updated=50
-   ->  Hash Join (actual rows=50.00 loops=1)
-         Hash Cond: (t.a = s.a)
-         ->  Seq Scan on ex_mtarget t (actual rows=50.00 loops=1)
-               Bloom Filter 1: keys=(a) checked=49 rejected=0 (0.0%)
-         ->  Hash (actual rows=100.00 loops=1)
-               Buckets: xxx  Batches: xxx  Memory Usage: xxx
-               Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=49 rejected=0
+   ->  Merge Join (actual rows=50.00 loops=1)
+         Merge Cond: (t.a = s.a)
+         ->  Sort (actual rows=50.00 loops=1)
+               Sort Key: t.a
+               Sort Method: quicksort  Memory: xxx
+               ->  Seq Scan on ex_mtarget t (actual rows=50.00 loops=1)
+         ->  Sort (actual rows=100.00 loops=1)
+               Sort Key: s.a
+               Sort Method: quicksort  Memory: xxx
                ->  Seq Scan on ex_msource s (actual rows=100.00 loops=1)
-(10 rows)
+(12 rows)
 
 -- only updates to selected tuples
 SELECT explain_merge('
 MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a
 WHEN MATCHED AND t.a < 10 THEN
 	UPDATE SET b = t.b + 1');
-                                      explain_merge                                       
-------------------------------------------------------------------------------------------
+                              explain_merge                              
+-------------------------------------------------------------------------
  Merge on ex_mtarget t (actual rows=0.00 loops=1)
    Tuples: updated=5 skipped=45
-   ->  Hash Join (actual rows=50.00 loops=1)
-         Hash Cond: (t.a = s.a)
-         ->  Seq Scan on ex_mtarget t (actual rows=50.00 loops=1)
-               Bloom Filter 1: keys=(a) checked=49 rejected=0 (0.0%)
-         ->  Hash (actual rows=100.00 loops=1)
-               Buckets: xxx  Batches: xxx  Memory Usage: xxx
-               Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=49 rejected=0
+   ->  Merge Join (actual rows=50.00 loops=1)
+         Merge Cond: (t.a = s.a)
+         ->  Sort (actual rows=50.00 loops=1)
+               Sort Key: t.a
+               Sort Method: quicksort  Memory: xxx
+               ->  Seq Scan on ex_mtarget t (actual rows=50.00 loops=1)
+         ->  Sort (actual rows=100.00 loops=1)
+               Sort Key: s.a
+               Sort Method: quicksort  Memory: xxx
                ->  Seq Scan on ex_msource s (actual rows=100.00 loops=1)
-(10 rows)
+(12 rows)
 
 -- updates + deletes
 SELECT explain_merge('
@@ -1679,19 +1680,21 @@ WHEN MATCHED AND t.a < 10 THEN
 	UPDATE SET b = t.b + 1
 WHEN MATCHED AND t.a >= 10 AND t.a <= 20 THEN
 	DELETE');
-                                      explain_merge                                       
-------------------------------------------------------------------------------------------
+                              explain_merge                              
+-------------------------------------------------------------------------
  Merge on ex_mtarget t (actual rows=0.00 loops=1)
    Tuples: updated=5 deleted=5 skipped=40
-   ->  Hash Join (actual rows=50.00 loops=1)
-         Hash Cond: (t.a = s.a)
-         ->  Seq Scan on ex_mtarget t (actual rows=50.00 loops=1)
-               Bloom Filter 1: keys=(a) checked=49 rejected=0 (0.0%)
-         ->  Hash (actual rows=100.00 loops=1)
-               Buckets: xxx  Batches: xxx  Memory Usage: xxx
-               Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=49 rejected=0
+   ->  Merge Join (actual rows=50.00 loops=1)
+         Merge Cond: (t.a = s.a)
+         ->  Sort (actual rows=50.00 loops=1)
+               Sort Key: t.a
+               Sort Method: quicksort  Memory: xxx
+               ->  Seq Scan on ex_mtarget t (actual rows=50.00 loops=1)
+         ->  Sort (actual rows=100.00 loops=1)
+               Sort Key: s.a
+               Sort Method: quicksort  Memory: xxx
                ->  Seq Scan on ex_msource s (actual rows=100.00 loops=1)
-(10 rows)
+(12 rows)
 
 -- only inserts
 SELECT explain_merge('
@@ -1788,20 +1791,21 @@ SELECT explain_merge('
 MERGE INTO ex_mtarget t USING ex_msource s ON t.a = s.a AND t.a < -1000
 WHEN MATCHED AND t.a < 10 THEN
 	DO NOTHING');
-                                      explain_merge                                      
------------------------------------------------------------------------------------------
+                             explain_merge                             
+-----------------------------------------------------------------------
  Merge on ex_mtarget t (actual rows=0.00 loops=1)
-   ->  Hash Join (actual rows=0.00 loops=1)
-         Hash Cond: (s.a = t.a)
-         ->  Seq Scan on ex_msource s (actual rows=1.00 loops=1)
-               Bloom Filter 1: keys=(a) checked=0 rejected=0 (0.0%)
-         ->  Hash (actual rows=0.00 loops=1)
-               Buckets: xxx  Batches: xxx  Memory Usage: xxx
-               Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=0 rejected=0
+   ->  Merge Join (actual rows=0.00 loops=1)
+         Merge Cond: (t.a = s.a)
+         ->  Sort (actual rows=0.00 loops=1)
+               Sort Key: t.a
+               Sort Method: quicksort  Memory: xxx
                ->  Seq Scan on ex_mtarget t (actual rows=0.00 loops=1)
                      Filter: (a < '-1000'::integer)
                      Rows Removed by Filter: 54
-(11 rows)
+         ->  Sort (never executed)
+               Sort Key: s.a
+               ->  Seq Scan on ex_msource s (never executed)
+(12 rows)
 
 DROP TABLE ex_msource, ex_mtarget;
 DROP FUNCTION explain_merge(text);
@@ -1827,10 +1831,8 @@ WHEN MATCHED AND t.c > s.cnt THEN
          Join Filter: (t.b < (SubPlan expr_1))
          ->  Seq Scan on public.tgt t
                Output: t.ctid, t.a, t.b
-               Bloom Filter 1: keys=(t.a)
          ->  Hash
                Output: s.a, s.b, s.c, s.d, s.ctid
-               Bloom Filter 1
                ->  Seq Scan on public.src s
                      Output: s.a, s.b, s.c, s.d, s.ctid
          SubPlan expr_1
@@ -1854,7 +1856,7 @@ WHEN MATCHED AND t.c > s.cnt THEN
                    ->  Seq Scan on public.ref r_1
                          Output: r_1.ab, r_1.cd
                          Filter: ((r_1.ab = (s.a + s.b)) AND (r_1.cd = (s.c - s.d)))
-(34 rows)
+(32 rows)
 
 DROP TABLE src, tgt, ref;
 -- Subqueries
diff --git a/src/test/regress/expected/select_parallel.out b/src/test/regress/expected/select_parallel.out
index 933921d1860..8f800a66c5e 100644
--- a/src/test/regress/expected/select_parallel.out
+++ b/src/test/regress/expected/select_parallel.out
@@ -1125,28 +1125,27 @@ reset role;
 explain (costs off, verbose)
   select count(*) from tenk1 a where (unique1, two) in
     (select unique1, row_number() over() from tenk1 b);
-                                       QUERY PLAN                                       
-----------------------------------------------------------------------------------------
+                                                                                         QUERY PLAN                                                                                          
+---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Aggregate
    Output: count(*)
-   ->  Hash Right Semi Join
-         Hash Cond: ((b.unique1 = a.unique1) AND ((row_number() OVER w1) = a.two))
-         ->  WindowAgg
-               Output: b.unique1, row_number() OVER w1
-               Window: w1 AS (ROWS UNBOUNDED PRECEDING)
-               ->  Gather
-                     Output: b.unique1
-                     Workers Planned: 4
-                     ->  Parallel Index Only Scan using tenk1_unique1 on public.tenk1 b
-                           Output: b.unique1
+   ->  Hash Semi Join
+         Hash Cond: ((a.unique1 = b.unique1) AND (a.two = (row_number() OVER w1)))
+         ->  Seq Scan on public.tenk1 a
+               Output: a.unique1, a.unique2, a.two, a.four, a.ten, a.twenty, a.hundred, a.thousand, a.twothousand, a.fivethous, a.tenthous, a.odd, a.even, a.stringu1, a.stringu2, a.string4
+               Bloom Filter 1: keys=(a.unique1, a.two)
          ->  Hash
-               Output: a.unique1, a.two
-               ->  Gather
-                     Output: a.unique1, a.two
-                     Workers Planned: 4
-                     ->  Parallel Seq Scan on public.tenk1 a
-                           Output: a.unique1, a.two
-(19 rows)
+               Output: b.unique1, (row_number() OVER w1)
+               Bloom Filter 1
+               ->  WindowAgg
+                     Output: b.unique1, row_number() OVER w1
+                     Window: w1 AS (ROWS UNBOUNDED PRECEDING)
+                     ->  Gather
+                           Output: b.unique1
+                           Workers Planned: 4
+                           ->  Parallel Index Only Scan using tenk1_unique1 on public.tenk1 b
+                                 Output: b.unique1
+(18 rows)
 
 -- LIMIT/OFFSET within sub-selects can't be pushed to workers.
 explain (costs off)
diff --git a/src/test/regress/expected/stats_ext.out b/src/test/regress/expected/stats_ext.out
index c3e0d6cc664..42dcf75f38f 100644
--- a/src/test/regress/expected/stats_ext.out
+++ b/src/test/regress/expected/stats_ext.out
@@ -3642,12 +3642,12 @@ SELECT * FROM sb_1 a, sb_2 b WHERE a.x = b.x AND a.y = b.y AND a.z = b.z;
                          QUERY PLAN                         
 ------------------------------------------------------------
  Hash Join
-   Hash Cond: ((b.x = a.x) AND (b.y = a.y) AND (b.z = a.z))
-   ->  Seq Scan on sb_2 b
+   Hash Cond: ((a.x = b.x) AND (a.y = b.y) AND (a.z = b.z))
+   ->  Seq Scan on sb_1 a
          Bloom Filter 1: keys=(x, y, z)
    ->  Hash
          Bloom Filter 1
-         ->  Seq Scan on sb_1 a
+         ->  Seq Scan on sb_2 b
 (7 rows)
 
 -- The ndistinct extended statistics on (x, y, z) provides more reliable value
@@ -3661,9 +3661,11 @@ SELECT * FROM sb_1 a, sb_2 b WHERE a.x = b.x AND a.y = b.y AND a.z = b.z;
  Hash Join
    Hash Cond: ((a.x = b.x) AND (a.y = b.y) AND (a.z = b.z))
    ->  Seq Scan on sb_1 a
+         Bloom Filter 1: keys=(x, y, z)
    ->  Hash
+         Bloom Filter 1
          ->  Seq Scan on sb_2 b
-(5 rows)
+(7 rows)
 
 -- Check that the Hash Join bucket size estimator detects equal clauses correctly.
 SET enable_nestloop = 'off';
diff --git a/src/test/regress/expected/subselect.out b/src/test/regress/expected/subselect.out
index f4cbc872844..e2564f877de 100644
--- a/src/test/regress/expected/subselect.out
+++ b/src/test/regress/expected/subselect.out
@@ -783,16 +783,14 @@ order by t1.a, t2.a;
                      Hash Cond: (t2.a = (t3.b + 1))
                      ->  Seq Scan on public.semijoin_unique_tbl t2
                            Output: t2.a, t2.b
-                           Bloom Filter 1: keys=(t2.a)
                      ->  Hash
                            Output: t3.a, t3.b
-                           Bloom Filter 1
                            ->  HashAggregate
                                  Output: t3.a, t3.b
                                  Group Key: (t3.a + 1), (t3.b + 1)
                                  ->  Seq Scan on public.semijoin_unique_tbl t3
                                        Output: t3.a, t3.b, (t3.a + 1), (t3.b + 1)
-(26 rows)
+(24 rows)
 
 -- encourage use of parallel plans
 set parallel_setup_cost=0;
@@ -1671,7 +1669,7 @@ select * from int4_tbl where
 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Nested Loop Semi Join
    Output: int4_tbl.f1
-   Join Filter: (b.ten = CASE WHEN (ANY (int4_tbl.f1 = (hashed SubPlan any_1).col1)) THEN int4_tbl.f1 ELSE NULL::integer END)
+   Join Filter: (CASE WHEN (ANY (int4_tbl.f1 = (hashed SubPlan any_1).col1)) THEN int4_tbl.f1 ELSE NULL::integer END = b.ten)
    ->  Seq Scan on public.int4_tbl
          Output: int4_tbl.f1
    ->  Seq Scan on public.tenk1 b
@@ -3678,14 +3676,15 @@ WHERE id NOT IN (
    Hash Cond: (not_null_tab.id = t2.id)
    ->  Seq Scan on not_null_tab
    ->  Hash
-         ->  Hash Join
-               Hash Cond: (t1.id = t2.id)
-               ->  Seq Scan on not_null_tab t1
-                     Bloom Filter 1: keys=(id)
-               ->  Hash
-                     Bloom Filter 1
+         ->  Merge Join
+               Merge Cond: (t1.id = t2.id)
+               ->  Sort
+                     Sort Key: t1.id
+                     ->  Seq Scan on not_null_tab t1
+               ->  Sort
+                     Sort Key: t2.id
                      ->  Seq Scan on not_null_tab t2
-(11 rows)
+(12 rows)
 
 -- ANTI JOIN: outer side is defined NOT NULL, inner side is forced nonnullable
 -- by qual clause
@@ -3726,8 +3725,11 @@ WHERE id NOT IN (
 );
                    QUERY PLAN                    
 -------------------------------------------------
- Merge Right Anti Join
-   Merge Cond: (t1.id = not_null_tab.id)
+ Merge Anti Join
+   Merge Cond: (not_null_tab.id = t1.id)
+   ->  Sort
+         Sort Key: not_null_tab.id
+         ->  Seq Scan on not_null_tab
    ->  Nested Loop Left Join
          ->  Merge Join
                Merge Cond: (t1.id = t2.id)
@@ -3739,9 +3741,6 @@ WHERE id NOT IN (
                      ->  Seq Scan on null_tab t2
          ->  Materialize
                ->  Seq Scan on null_tab t3
-   ->  Sort
-         Sort Key: not_null_tab.id
-         ->  Seq Scan on not_null_tab
 (16 rows)
 
 -- ANTI JOIN: outer side is defined NOT NULL and is not nulled by outer join,
diff --git a/src/test/regress/expected/updatable_views.out b/src/test/regress/expected/updatable_views.out
index 36352e6c282..28c7b8db851 100644
--- a/src/test/regress/expected/updatable_views.out
+++ b/src/test/regress/expected/updatable_views.out
@@ -2779,14 +2779,12 @@ EXPLAIN (costs off) UPDATE rw_view1 SET a = a + 5;
    ->  Hash Join
          Hash Cond: (b.a = r.a)
          ->  Seq Scan on base_tbl b
-               Bloom Filter 1: keys=(a)
          ->  Hash
-               Bloom Filter 1
                ->  Seq Scan on ref_tbl r
    SubPlan exists_1
      ->  Index Only Scan using ref_tbl_pkey on ref_tbl r_1
            Index Cond: (a = b.a)
-(11 rows)
+(9 rows)
 
 DROP TABLE base_tbl, ref_tbl CASCADE;
 NOTICE:  drop cascades to view rw_view1
@@ -3526,17 +3524,18 @@ EXPLAIN (COSTS OFF) UPDATE v2 SET a = 1;
  Update on t1
    InitPlan exists_1
      ->  Result
-   ->  Hash Join
-         Hash Cond: (t1.a = v1.a)
-         ->  Seq Scan on t1
-               Bloom Filter 1: keys=(a)
-         ->  Hash
-               Bloom Filter 1
+   ->  Merge Join
+         Merge Cond: (t1.a = v1.a)
+         ->  Sort
+               Sort Key: t1.a
+               ->  Seq Scan on t1
+         ->  Sort
+               Sort Key: v1.a
                ->  Subquery Scan on v1
                      ->  Result
                            One-Time Filter: (InitPlan exists_1).col1
                            ->  Seq Scan on t1 t1_1
-(13 rows)
+(14 rows)
 
 DROP VIEW v2;
 DROP VIEW v1;
diff --git a/src/test/regress/expected/window.out b/src/test/regress/expected/window.out
index d55bc161159..0b31f3b39b2 100644
--- a/src/test/regress/expected/window.out
+++ b/src/test/regress/expected/window.out
@@ -4360,14 +4360,15 @@ WHERE s.c = 1;
          Run Condition: (ntile(e2.salary) OVER w1 <= 1)
          ->  Sort
                Sort Key: e1.depname
-               ->  Hash Join
-                     Hash Cond: (e1.empno = e2.empno)
-                     ->  Seq Scan on empsalary e1
-                           Bloom Filter 1: keys=(empno)
-                     ->  Hash
-                           Bloom Filter 1
+               ->  Merge Join
+                     Merge Cond: (e1.empno = e2.empno)
+                     ->  Sort
+                           Sort Key: e1.empno
+                           ->  Seq Scan on empsalary e1
+                     ->  Sort
+                           Sort Key: e2.empno
                            ->  Seq Scan on empsalary e2
-(14 rows)
+(15 rows)
 
 -- Ensure the run condition optimization is used in cases where the WindowFunc
 -- has a Var from another query level
diff --git a/src/test/regress/expected/with.out b/src/test/regress/expected/with.out
index 821ea3c5b0e..addb24896be 100644
--- a/src/test/regress/expected/with.out
+++ b/src/test/regress/expected/with.out
@@ -751,20 +751,22 @@ select * from search_graph order by seq;
      ->  Recursive Union
            ->  Seq Scan on pg_temp.graph0 g
                  Output: g.f, g.t, g.label, ARRAY[ROW(g.f, g.t)]
-           ->  Hash Join
+           ->  Merge Join
                  Output: g_1.f, g_1.t, g_1.label, array_cat(sg.seq, ARRAY[ROW(g_1.f, g_1.t)])
-                 Hash Cond: (g_1.f = sg.t)
-                 ->  Seq Scan on pg_temp.graph0 g_1
+                 Merge Cond: (g_1.f = sg.t)
+                 ->  Sort
                        Output: g_1.f, g_1.t, g_1.label
-                       Bloom Filter 1: keys=(g_1.f)
-                 ->  Hash
+                       Sort Key: g_1.f
+                       ->  Seq Scan on pg_temp.graph0 g_1
+                             Output: g_1.f, g_1.t, g_1.label
+                 ->  Sort
                        Output: sg.seq, sg.t
-                       Bloom Filter 1
+                       Sort Key: sg.t
                        ->  WorkTable Scan on search_graph sg
                              Output: sg.seq, sg.t
    ->  CTE Scan on search_graph
          Output: search_graph.f, search_graph.t, search_graph.label, search_graph.seq
-(20 rows)
+(22 rows)
 
 with recursive search_graph(f, t, label) as (
 	select * from graph0 g
@@ -822,20 +824,22 @@ select * from search_graph order by seq;
      ->  Recursive Union
            ->  Seq Scan on pg_temp.graph0 g
                  Output: g.f, g.t, g.label, ROW('0'::bigint, g.f, g.t)
-           ->  Hash Join
+           ->  Merge Join
                  Output: g_1.f, g_1.t, g_1.label, ROW(int8inc((sg.seq)."*DEPTH*"), g_1.f, g_1.t)
-                 Hash Cond: (g_1.f = sg.t)
-                 ->  Seq Scan on pg_temp.graph0 g_1
+                 Merge Cond: (g_1.f = sg.t)
+                 ->  Sort
                        Output: g_1.f, g_1.t, g_1.label
-                       Bloom Filter 1: keys=(g_1.f)
-                 ->  Hash
+                       Sort Key: g_1.f
+                       ->  Seq Scan on pg_temp.graph0 g_1
+                             Output: g_1.f, g_1.t, g_1.label
+                 ->  Sort
                        Output: sg.seq, sg.t
-                       Bloom Filter 1
+                       Sort Key: sg.t
                        ->  WorkTable Scan on search_graph sg
                              Output: sg.seq, sg.t
    ->  CTE Scan on search_graph
          Output: search_graph.f, search_graph.t, search_graph.label, search_graph.seq
-(20 rows)
+(22 rows)
 
 with recursive search_graph(f, t, label) as (
 	select * from graph0 g
@@ -1091,20 +1095,20 @@ select * from search_graph;
  5 | 1 | arc 5 -> 1 | f        | {"(5,1)"}
  1 | 2 | arc 1 -> 2 | f        | {"(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | f        | {"(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | f        | {"(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | f        | {"(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | f        | {"(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | f        | {"(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | f        | {"(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | f        | {"(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | f        | {"(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | f        | {"(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | f        | {"(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | f        | {"(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | f        | {"(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | f        | {"(1,4)","(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | f        | {"(1,4)","(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | f        | {"(1,4)","(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | t        | {"(1,4)","(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | t        | {"(4,5)","(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | t        | {"(5,1)","(1,4)","(4,5)","(5,1)"}
  2 | 3 | arc 2 -> 3 | f        | {"(1,4)","(4,5)","(5,1)","(1,2)","(2,3)"}
@@ -1129,20 +1133,20 @@ select * from search_graph;
  5 | 1 | arc 5 -> 1 | f        | {"(5,1)"}
  1 | 2 | arc 1 -> 2 | f        | {"(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | f        | {"(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | f        | {"(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | f        | {"(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | f        | {"(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | f        | {"(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | f        | {"(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | f        | {"(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | f        | {"(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | f        | {"(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | f        | {"(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | f        | {"(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | f        | {"(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | f        | {"(1,4)","(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | f        | {"(1,4)","(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | f        | {"(1,4)","(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | t        | {"(1,4)","(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | t        | {"(4,5)","(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | t        | {"(5,1)","(1,4)","(4,5)","(5,1)"}
  2 | 3 | arc 2 -> 3 | f        | {"(1,4)","(4,5)","(5,1)","(1,2)","(2,3)"}
@@ -1204,19 +1208,21 @@ select * from search_graph;
      ->  Recursive Union
            ->  Seq Scan on pg_temp.graph g
                  Output: g.f, g.t, g.label, false, ARRAY[ROW(g.f, g.t)]
-           ->  Hash Join
+           ->  Merge Join
                  Output: g_1.f, g_1.t, g_1.label, CASE WHEN (ROW(g_1.f, g_1.t) = ANY (sg.path)) THEN true ELSE false END, array_cat(sg.path, ARRAY[ROW(g_1.f, g_1.t)])
-                 Hash Cond: (g_1.f = sg.t)
-                 ->  Seq Scan on pg_temp.graph g_1
+                 Merge Cond: (g_1.f = sg.t)
+                 ->  Sort
                        Output: g_1.f, g_1.t, g_1.label
-                       Bloom Filter 1: keys=(g_1.f)
-                 ->  Hash
+                       Sort Key: g_1.f
+                       ->  Seq Scan on pg_temp.graph g_1
+                             Output: g_1.f, g_1.t, g_1.label
+                 ->  Sort
                        Output: sg.path, sg.t
-                       Bloom Filter 1
+                       Sort Key: sg.t
                        ->  WorkTable Scan on search_graph sg
                              Output: sg.path, sg.t
                              Filter: (NOT sg.is_cycle)
-(18 rows)
+(20 rows)
 
 with recursive search_graph(f, t, label) as (
 	select * from graph g
@@ -1236,20 +1242,20 @@ select * from search_graph;
  5 | 1 | arc 5 -> 1 | f        | {"(5,1)"}
  1 | 2 | arc 1 -> 2 | f        | {"(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | f        | {"(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | f        | {"(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | f        | {"(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | f        | {"(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | f        | {"(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | f        | {"(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | f        | {"(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | f        | {"(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | f        | {"(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | f        | {"(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | f        | {"(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | f        | {"(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | f        | {"(1,4)","(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | f        | {"(1,4)","(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | f        | {"(1,4)","(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | t        | {"(1,4)","(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | t        | {"(4,5)","(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | t        | {"(5,1)","(1,4)","(4,5)","(5,1)"}
  2 | 3 | arc 2 -> 3 | f        | {"(1,4)","(4,5)","(5,1)","(1,2)","(2,3)"}
@@ -1273,20 +1279,20 @@ select * from search_graph;
  5 | 1 | arc 5 -> 1 | N        | {"(5,1)"}
  1 | 2 | arc 1 -> 2 | N        | {"(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | N        | {"(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | N        | {"(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | N        | {"(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | N        | {"(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | N        | {"(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | N        | {"(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | N        | {"(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | N        | {"(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | N        | {"(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | N        | {"(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | N        | {"(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | N        | {"(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | N        | {"(1,4)","(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | N        | {"(1,4)","(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | N        | {"(1,4)","(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | N        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | Y        | {"(1,4)","(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | N        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | Y        | {"(4,5)","(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | Y        | {"(5,1)","(1,4)","(4,5)","(5,1)"}
  2 | 3 | arc 2 -> 3 | N        | {"(1,4)","(4,5)","(5,1)","(1,2)","(2,3)"}
@@ -1438,20 +1444,20 @@ select * from search_graph;
  5 | 1 | arc 5 -> 1 | {"(5,1)"}                                 | f        | {"(5,1)"}
  1 | 2 | arc 1 -> 2 | {"(5,1)","(1,2)"}                         | f        | {"(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | {"(5,1)","(1,3)"}                         | f        | {"(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | {"(1,2)","(2,3)"}                         | f        | {"(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | {"(5,1)","(1,4)"}                         | f        | {"(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | {"(1,2)","(2,3)"}                         | f        | {"(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | {"(1,4)","(4,5)"}                         | f        | {"(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | {"(4,5)","(5,1)"}                         | f        | {"(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | {"(4,5)","(5,1)","(1,2)"}                 | f        | {"(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | {"(4,5)","(5,1)","(1,3)"}                 | f        | {"(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | {"(5,1)","(1,2)","(2,3)"}                 | f        | {"(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | {"(4,5)","(5,1)","(1,4)"}                 | f        | {"(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | {"(5,1)","(1,2)","(2,3)"}                 | f        | {"(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | {"(5,1)","(1,4)","(4,5)"}                 | f        | {"(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | {"(1,4)","(4,5)","(5,1)"}                 | f        | {"(1,4)","(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | {"(1,4)","(4,5)","(5,1)","(1,2)"}         | f        | {"(1,4)","(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | {"(1,4)","(4,5)","(5,1)","(1,3)"}         | f        | {"(1,4)","(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | {"(4,5)","(5,1)","(1,2)","(2,3)"}         | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | {"(1,4)","(4,5)","(5,1)","(1,4)"}         | t        | {"(1,4)","(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | {"(4,5)","(5,1)","(1,2)","(2,3)"}         | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | {"(4,5)","(5,1)","(1,4)","(4,5)"}         | t        | {"(4,5)","(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | {"(5,1)","(1,4)","(4,5)","(5,1)"}         | t        | {"(5,1)","(1,4)","(4,5)","(5,1)"}
  2 | 3 | arc 2 -> 3 | {"(1,4)","(4,5)","(5,1)","(1,2)","(2,3)"} | f        | {"(1,4)","(4,5)","(5,1)","(1,2)","(2,3)"}
@@ -1476,20 +1482,20 @@ select * from search_graph;
  5 | 1 | arc 5 -> 1 | (0,5,1) | f        | {"(5,1)"}
  1 | 2 | arc 1 -> 2 | (1,1,2) | f        | {"(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | (1,1,3) | f        | {"(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | (1,2,3) | f        | {"(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | (1,1,4) | f        | {"(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | (1,2,3) | f        | {"(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | (1,4,5) | f        | {"(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | (1,5,1) | f        | {"(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | (2,1,2) | f        | {"(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | (2,1,3) | f        | {"(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | (2,2,3) | f        | {"(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | (2,1,4) | f        | {"(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | (2,2,3) | f        | {"(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | (2,4,5) | f        | {"(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | (2,5,1) | f        | {"(1,4)","(4,5)","(5,1)"}
  1 | 2 | arc 1 -> 2 | (3,1,2) | f        | {"(1,4)","(4,5)","(5,1)","(1,2)"}
  1 | 3 | arc 1 -> 3 | (3,1,3) | f        | {"(1,4)","(4,5)","(5,1)","(1,3)"}
- 2 | 3 | arc 2 -> 3 | (3,2,3) | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  1 | 4 | arc 1 -> 4 | (3,1,4) | t        | {"(1,4)","(4,5)","(5,1)","(1,4)"}
+ 2 | 3 | arc 2 -> 3 | (3,2,3) | f        | {"(4,5)","(5,1)","(1,2)","(2,3)"}
  4 | 5 | arc 4 -> 5 | (3,4,5) | t        | {"(4,5)","(5,1)","(1,4)","(4,5)"}
  5 | 1 | arc 5 -> 1 | (3,5,1) | t        | {"(5,1)","(1,4)","(4,5)","(5,1)"}
  2 | 3 | arc 2 -> 3 | (4,2,3) | f        | {"(1,4)","(4,5)","(5,1)","(1,2)","(2,3)"}
@@ -1669,20 +1675,20 @@ select * from v_cycle1;
  5 | 1 | arc 5 -> 1
  1 | 2 | arc 1 -> 2
  1 | 3 | arc 1 -> 3
- 2 | 3 | arc 2 -> 3
  1 | 4 | arc 1 -> 4
+ 2 | 3 | arc 2 -> 3
  4 | 5 | arc 4 -> 5
  5 | 1 | arc 5 -> 1
  1 | 2 | arc 1 -> 2
  1 | 3 | arc 1 -> 3
- 2 | 3 | arc 2 -> 3
  1 | 4 | arc 1 -> 4
+ 2 | 3 | arc 2 -> 3
  4 | 5 | arc 4 -> 5
  5 | 1 | arc 5 -> 1
  1 | 2 | arc 1 -> 2
  1 | 3 | arc 1 -> 3
- 2 | 3 | arc 2 -> 3
  1 | 4 | arc 1 -> 4
+ 2 | 3 | arc 2 -> 3
  4 | 5 | arc 4 -> 5
  5 | 1 | arc 5 -> 1
  2 | 3 | arc 2 -> 3
@@ -1699,20 +1705,20 @@ select * from v_cycle2;
  5 | 1 | arc 5 -> 1
  1 | 2 | arc 1 -> 2
  1 | 3 | arc 1 -> 3
- 2 | 3 | arc 2 -> 3
  1 | 4 | arc 1 -> 4
+ 2 | 3 | arc 2 -> 3
  4 | 5 | arc 4 -> 5
  5 | 1 | arc 5 -> 1
  1 | 2 | arc 1 -> 2
  1 | 3 | arc 1 -> 3
- 2 | 3 | arc 2 -> 3
  1 | 4 | arc 1 -> 4
+ 2 | 3 | arc 2 -> 3
  4 | 5 | arc 4 -> 5
  5 | 1 | arc 5 -> 1
  1 | 2 | arc 1 -> 2
  1 | 3 | arc 1 -> 3
- 2 | 3 | arc 2 -> 3
  1 | 4 | arc 1 -> 4
+ 2 | 3 | arc 2 -> 3
  4 | 5 | arc 4 -> 5
  5 | 1 | arc 5 -> 1
  2 | 3 | arc 2 -> 3
-- 
2.50.1 (Apple Git-155)

