From ec44f699a1a2976712a73be75493794a8c91623d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Mon, 27 Jul 2026 15:24:24 +0200
Subject: [PATCH v8 15/20] Sort filters pushed-down to a scan by selectivity

Initially this was an attempt to stabilize plans in regression tests,
but it's not sufficient - the join order may change independently of
this, as the filter pushdown eliminates impact on intermediate join
result sizes.

Still, it's desirable to evaluate the most selective filters first.
Right now it's based only on selectivity, but it should consider the
cost of probing the filter too (number of clauses, filter size, ...).

The order in which filters end up pushed to the scan is somewhat
volatile, because the filter pushdown limits the importance of join
order - the cardinality gets reduced by the filter, not by the join
which happens sometime later. That means the order of filter-producing
joins has mostly the same cost.

XXX Maybe we should still influence the cost based on the expected
selectivity of the filter it generates, by the "risk" of the filter
being less effective, etc.
---
 .../pg_plan_advice/expected/join_order.out    |  4 +--
 .../expected/pg_stash_advice.out              | 10 +++----
 src/backend/executor/nodeHashjoin.c           | 29 +++++++++++++++++++
 src/backend/optimizer/plan/createplan.c       |  1 +
 src/include/nodes/plannodes.h                 |  1 +
 5 files changed, 38 insertions(+), 7 deletions(-)

diff --git a/contrib/pg_plan_advice/expected/join_order.out b/contrib/pg_plan_advice/expected/join_order.out
index ab4125b4243..ded25c4010b 100644
--- a/contrib/pg_plan_advice/expected/join_order.out
+++ b/contrib/pg_plan_advice/expected/join_order.out
@@ -34,8 +34,8 @@ SELECT * FROM jo_fact f
    ->  Hash Join
          Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on jo_fact f
-               Bloom Filter 1: keys=(dim1_id)
                Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
                Bloom Filter 1
                ->  Seq Scan on jo_dim1 d1
@@ -67,8 +67,8 @@ SELECT * FROM jo_fact f
    ->  Hash Join
          Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on jo_fact f
-               Bloom Filter 1: keys=(dim1_id)
                Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
                Bloom Filter 1
                ->  Seq Scan on jo_dim1 d1
diff --git a/contrib/pg_stash_advice/expected/pg_stash_advice.out b/contrib/pg_stash_advice/expected/pg_stash_advice.out
index e14e5af4677..6372bf36c2c 100644
--- a/contrib/pg_stash_advice/expected/pg_stash_advice.out
+++ b/contrib/pg_stash_advice/expected/pg_stash_advice.out
@@ -64,8 +64,8 @@ SELECT * FROM aa_fact f LEFT JOIN aa_dim1 d1 ON f.dim1_id = d1.id
    ->  Hash Join
          Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
-               Bloom Filter 1: keys=(dim1_id)
                Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
                Bloom Filter 1
                ->  Seq Scan on aa_dim1 d1
@@ -95,8 +95,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
    ->  Hash Join
          Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
-               Bloom Filter 1: keys=(dim1_id)
                Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
                Bloom Filter 1
                ->  Index Scan using aa_dim1_pkey on aa_dim1 d1
@@ -128,8 +128,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
    ->  Hash Join
          Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
-               Bloom Filter 1: keys=(dim1_id)
                Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
                Bloom Filter 1
                ->  Seq Scan on aa_dim1 d1
@@ -221,8 +221,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
    ->  Hash Join
          Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
-               Bloom Filter 1: keys=(dim1_id)
                Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
                Bloom Filter 1
                ->  Seq Scan on aa_dim1 d1
@@ -288,8 +288,8 @@ EXPLAIN (COSTS OFF) SELECT * FROM aa_fact f
    ->  Hash Join
          Hash Cond: (f.dim1_id = d1.id)
          ->  Seq Scan on aa_fact f
-               Bloom Filter 1: keys=(dim1_id)
                Bloom Filter 2: keys=(dim2_id)
+               Bloom Filter 1: keys=(dim1_id)
          ->  Hash
                Bloom Filter 1
                ->  Seq Scan on aa_dim1 d1
diff --git a/src/backend/executor/nodeHashjoin.c b/src/backend/executor/nodeHashjoin.c
index e532e4cfd0a..2dbbfc3a34f 100644
--- a/src/backend/executor/nodeHashjoin.c
+++ b/src/backend/executor/nodeHashjoin.c
@@ -2350,6 +2350,28 @@ ExecBloomFilters(List *filters, ExprContext *econtext)
 	return true;
 }
 
+static int
+list_filter_selectivity_cmp(const ListCell *p1, const ListCell *p2)
+{
+	struct BloomFilter *bf1 = lfirst(p1);
+	struct BloomFilter *bf2 = lfirst(p2);
+
+	/* compare by selectivity first (prefer more selective) */
+	if (bf1->selectivity < bf2->selectivity)
+		return -1;
+	else if (bf1->selectivity > bf2->selectivity)
+		return 1;
+
+	/* then by the producer ID, as a tie break */
+	if (bf1->producer_id < bf2->producer_id)
+		return -1;
+	else if (bf1->producer_id > bf2->producer_id)
+		return 1;
+
+	/* should be the same filter */
+	return 0;
+}
+
 /*
  * ExecInitBloomFilters
  *		Initialize state for pushed-down bloom filters.
@@ -2388,6 +2410,13 @@ ExecInitBloomFilters(PlanState *planstate, TupleTableSlot *output_slot)
 	if (plan->bloom_filters == NIL)
 		return;
 
+	/*
+	 * XXX Sort filters from the most to least selective ones. Ideally,
+	 * we'd consider how expensive it's to evaluate the filter, i.e. how
+	 * many keys/clauses it has, how large it is, etc. Left for future.
+	 */
+	list_sort(plan->bloom_filters, list_filter_selectivity_cmp);
+
 	oldctx = MemoryContextSwitchTo(estate->es_query_cxt);
 
 	foreach(lc, plan->bloom_filters)
diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c
index 6a496e4df6c..f6ade21b13e 100644
--- a/src/backend/optimizer/plan/createplan.c
+++ b/src/backend/optimizer/plan/createplan.c
@@ -4947,6 +4947,7 @@ try_push_bloom_filter(PlannerInfo *root, HashJoin *hj, Plan *outer_plan,
 	bf->hashops = list_copy(hj->hashoperators);
 	bf->hashcollations = list_copy(hj->hashcollations);
 	bf->producer_id = hj->bloom_filter_id;
+	bf->selectivity = f->selectivity;
 
 	recipient->bloom_filters = lappend(recipient->bloom_filters, bf);
 
diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h
index 18b725aee61..5fe975f1c48 100644
--- a/src/include/nodes/plannodes.h
+++ b/src/include/nodes/plannodes.h
@@ -286,6 +286,7 @@ typedef struct BloomFilter
 	List	   *hashops;
 	List	   *hashcollations;
 	int			producer_id;
+	Selectivity	selectivity;	/* estimates selectivity */
 } BloomFilter;
 
 /* ----------------
-- 
2.55.0

