From a4c0f7cfeaec1f4de4e33e545da595fcf853322d Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Mon, 27 Jul 2026 19:08:53 +0200
Subject: [PATCH v9 20/21] Test a snowflake join with multi-column FK join
 clauses

---
 .../expected/hashjoin_bloom_snowflake.out     | 183 ++++++++++++++++++
 .../regress/sql/hashjoin_bloom_snowflake.sql  |  98 ++++++++++
 2 files changed, 281 insertions(+)

diff --git a/src/test/regress/expected/hashjoin_bloom_snowflake.out b/src/test/regress/expected/hashjoin_bloom_snowflake.out
index dbe7663614f..b06e42696d6 100644
--- a/src/test/regress/expected/hashjoin_bloom_snowflake.out
+++ b/src/test/regress/expected/hashjoin_bloom_snowflake.out
@@ -384,3 +384,186 @@ DROP TABLE bloom_snowflake_dim_2_1;
 DROP TABLE bloom_snowflake_dim_2_2;
 DROP TABLE bloom_snowflake_dim_2;
 DROP TABLE bloom_snowflake_fact;
+-- snowflake queries on multi-column FK joins
+CREATE TABLE bloom_snowflake_multi_dim_1_1 (a int, b int, r real, primary key (a, b));
+CREATE TABLE bloom_snowflake_multi_dim_1_2 (a int, b int, r real, primary key (a, b));
+CREATE TABLE bloom_snowflake_multi_dim_1 (
+	a int, b int,
+	id11a int, id11b int,
+	id12a int, id12b int,
+	r real,
+	foreign key (id11a, id11b) references bloom_snowflake_multi_dim_1_1(a, b),
+	foreign key (id12a, id12b) references bloom_snowflake_multi_dim_1_2(a, b),
+	primary key (a, b));
+CREATE TABLE bloom_snowflake_multi_dim_2_1 (a int, b int, r real, primary key (a, b));
+CREATE TABLE bloom_snowflake_multi_dim_2_2 (a int, b int, r real, primary key (a, b));
+CREATE TABLE bloom_snowflake_multi_dim_2 (
+	a int, b int,
+	id21a int, id21b int,
+	id22a int, id22b int,
+	r real,
+	foreign key (id21a, id21b) references bloom_snowflake_multi_dim_2_1(a, b),
+	foreign key (id22a, id22b) references bloom_snowflake_multi_dim_2_2(a, b),
+	primary key (a, b));
+CREATE TABLE bloom_snowflake_multi_fact (
+	id1a int, id1b int,
+	id2a int, id2b int,
+	padding text,
+	foreign key (id1a, id1b) references bloom_snowflake_multi_dim_1(a, b),
+	foreign key (id2a, id2b) references bloom_snowflake_multi_dim_2(a, b));
+SELECT setseed(0.5);
+ setseed 
+---------
+ 
+(1 row)
+
+INSERT INTO bloom_snowflake_multi_dim_1_1 SELECT i, i, random() FROM generate_series(1, 1000) s(i);
+INSERT INTO bloom_snowflake_multi_dim_1_2 SELECT i, i, random() FROM generate_series(1, 1000) s(i);
+WITH d AS (SELECT i, 1 + mod((100000 * random())::int, 1000) AS a, 1 + mod((100000 * random())::int, 1000) AS b FROM generate_series(1, 1000) s(i))
+INSERT INTO bloom_snowflake_multi_dim_1 SELECT i, i, a, a, b, b, random() FROM d;
+INSERT INTO bloom_snowflake_multi_dim_2_1 SELECT i, i, random() FROM generate_series(1, 10000) s(i);
+INSERT INTO bloom_snowflake_multi_dim_2_2 SELECT i, i, random() FROM generate_series(1, 10000) s(i);
+WITH d AS (SELECT i, 1 + mod((100000 * random())::int, 1000) AS a, 1 + mod((100000 * random())::int, 1000) AS b FROM generate_series(1, 1000) s(i))
+INSERT INTO bloom_snowflake_multi_dim_2 SELECT i, i, a, a, b, b, random() FROM d;
+WITH d AS (SELECT 1 + mod((100000 * random())::int, 1000) AS a, 1 + mod((100000 * random())::int, 1000) AS b, md5(i::text) AS p FROM generate_series(1, 100000) AS s(i))
+INSERT INTO bloom_snowflake_multi_fact
+SELECT
+    a, a, b, b, p
+FROM d;
+SET default_statistics_target = 1000;
+VACUUM ANALYZE;
+-- increase the accepted build size (includes the fact)
+SET bloom_filter_pushdown_max_build_relids = 4;
+EXPLAIN (ANALYZE, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
+SELECT *
+FROM bloom_snowflake_multi_fact f
+JOIN bloom_snowflake_multi_dim_1 d1 ON (f.id1a = d1.a AND f.id1b = d1.b)
+JOIN bloom_snowflake_multi_dim_1_1 d11 ON (d1.id11a = d11.a AND d1.id11b = d11.b)
+JOIN bloom_snowflake_multi_dim_1_2 d12 ON (d1.id12a = d12.a AND d1.id12b = d12.b)
+WHERE d11.r < 0.45 AND d12.r < 0.55;
+                                                                   QUERY PLAN                                                                   
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join  (cost=76.27..2292.04 rows=24628 width=101) (actual rows=23530.00 loops=1)
+   Hash Cond: ((f.id1a = d1.a) AND (f.id1b = d1.b))
+   ->  Seq Scan on bloom_snowflake_multi_fact f  (cost=0.00..2031.00 rows=24628 width=49) (actual rows=23531.00 loops=1)
+         Bloom Filter 3: keys=(id1a, id1b) expected=24.6% checked=99999 rejected=76469 (76.5%)
+   ->  Hash  (cost=72.58..72.58 rows=246 width=52) (actual rows=234.00 loops=1)
+         Buckets: 1024  Batches: 1  Memory Usage: 28kB
+         Bloom Filter 3: bits=8388608 hashes=10 memory=1024kB checked=99999 rejected=76469
+         ->  Hash Join  (cost=52.00..72.58 rows=246 width=52) (actual rows=234.00 loops=1)
+               Hash Cond: ((d1.id12a = d12.a) AND (d1.id12b = d12.b))
+               ->  Hash Join  (cost=25.09..44.38 rows=246 width=40) (actual rows=234.00 loops=1)
+                     Hash Cond: ((d1.id11a = d11.a) AND (d1.id11b = d11.b))
+                     ->  Seq Scan on bloom_snowflake_multi_dim_1 d1  (cost=0.00..18.00 rows=246 width=28) (actual rows=235.00 loops=1)
+                           Bloom Filter 1: keys=(id11a, id11b) expected=43.9% checked=996 rejected=569 (57.1%)
+                           Bloom Filter 2: keys=(id12a, id12b) expected=56.1% checked=431 rejected=196 (45.5%)
+                     ->  Hash  (cost=18.50..18.50 rows=439 width=12) (actual rows=440.00 loops=1)
+                           Buckets: 1024  Batches: 1  Memory Usage: 27kB
+                           Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=996 rejected=569
+                           ->  Seq Scan on bloom_snowflake_multi_dim_1_1 d11  (cost=0.00..18.50 rows=439 width=12) (actual rows=440.00 loops=1)
+                                 Filter: (r < '0.45'::double precision)
+                                 Rows Removed by Filter: 560
+               ->  Hash  (cost=18.50..18.50 rows=561 width=12) (actual rows=562.00 loops=1)
+                     Buckets: 1024  Batches: 1  Memory Usage: 33kB
+                     Bloom Filter 2: bits=8388608 hashes=10 memory=1024kB checked=431 rejected=196
+                     ->  Seq Scan on bloom_snowflake_multi_dim_1_2 d12  (cost=0.00..18.50 rows=561 width=12) (actual rows=562.00 loops=1)
+                           Filter: (r < '0.55'::double precision)
+                           Rows Removed by Filter: 438
+(26 rows)
+
+EXPLAIN (ANALYZE, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
+SELECT *
+FROM bloom_snowflake_multi_fact f
+JOIN bloom_snowflake_multi_dim_1 d1 ON (f.id1a = d1.a AND f.id1b = d1.b)
+JOIN bloom_snowflake_multi_dim_1_1 d11 ON (d1.id11a = d11.a AND d1.id11b = d11.b)
+JOIN bloom_snowflake_multi_dim_1_2 d12 ON (d1.id12a = d12.a AND d1.id12b = d12.b)
+WHERE d11.r < 0.75 AND d12.r < 0.75;
+                                                                   QUERY PLAN                                                                   
+------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join  (cost=94.81..2542.85 rows=55564 width=101) (actual rows=55598.00 loops=1)
+   Hash Cond: ((f.id1a = d1.a) AND (f.id1b = d1.b))
+   ->  Seq Scan on bloom_snowflake_multi_fact f  (cost=0.00..2031.00 rows=55564 width=49) (actual rows=55598.00 loops=1)
+         Bloom Filter 1: keys=(id1a, id1b) expected=55.6% checked=99999 rejected=44402 (44.4%)
+   ->  Hash  (cost=86.47..86.47 rows=556 width=52) (actual rows=555.00 loops=1)
+         Buckets: 1024  Batches: 1  Memory Usage: 54kB
+         Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=99999 rejected=44402
+         ->  Hash Join  (cost=59.36..86.47 rows=556 width=52) (actual rows=555.00 loops=1)
+               Hash Cond: ((d1.id12a = d12.a) AND (d1.id12b = d12.b))
+               ->  Hash Join  (cost=29.51..52.76 rows=734 width=40) (actual rows=737.00 loops=1)
+                     Hash Cond: ((d1.id11a = d11.a) AND (d1.id11b = d11.b))
+                     ->  Seq Scan on bloom_snowflake_multi_dim_1 d1  (cost=0.00..18.00 rows=1000 width=28) (actual rows=1000.00 loops=1)
+                     ->  Hash  (cost=18.50..18.50 rows=734 width=12) (actual rows=735.00 loops=1)
+                           Buckets: 1024  Batches: 1  Memory Usage: 40kB
+                           ->  Seq Scan on bloom_snowflake_multi_dim_1_1 d11  (cost=0.00..18.50 rows=734 width=12) (actual rows=735.00 loops=1)
+                                 Filter: (r < '0.75'::double precision)
+                                 Rows Removed by Filter: 265
+               ->  Hash  (cost=18.50..18.50 rows=757 width=12) (actual rows=758.00 loops=1)
+                     Buckets: 1024  Batches: 1  Memory Usage: 41kB
+                     ->  Seq Scan on bloom_snowflake_multi_dim_1_2 d12  (cost=0.00..18.50 rows=757 width=12) (actual rows=758.00 loops=1)
+                           Filter: (r < '0.75'::double precision)
+                           Rows Removed by Filter: 242
+(22 rows)
+
+EXPLAIN (ANALYZE, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
+SELECT *
+FROM bloom_snowflake_multi_fact f
+JOIN bloom_snowflake_multi_dim_1 d1 ON (f.id1a = d1.a AND f.id1b = d1.b)
+JOIN bloom_snowflake_multi_dim_1_1 d11 ON (d1.id11a = d11.a AND d1.id11b = d11.b)
+JOIN bloom_snowflake_multi_dim_1_2 d12 ON (d1.id12a = d12.a AND d1.id12b = d12.b)
+JOIN bloom_snowflake_multi_dim_2 d2 ON (f.id2a = d2.a AND f.id2b = d2.b)
+JOIN bloom_snowflake_multi_dim_2_1 d21 ON (d2.id21a = d21.a AND d2.id21b = d21.b)
+JOIN bloom_snowflake_multi_dim_2_2 d22 ON (d2.id22a = d22.a AND d2.id22b = d22.b)
+WHERE d11.r < 0.75 AND d12.r < 0.75;
+                                                                                             QUERY PLAN                                                                                              
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Hash Join  (cost=331.99..3197.32 rows=55564 width=153) (actual rows=55598.00 loops=1)
+   Hash Cond: ((f.id1a = d1.a) AND (f.id1b = d1.b))
+   ->  Hash Join  (cost=237.18..2685.47 rows=55564 width=101) (actual rows=55598.00 loops=1)
+         Hash Cond: ((f.id2a = d2.a) AND (f.id2b = d2.b))
+         ->  Seq Scan on bloom_snowflake_multi_fact f  (cost=0.00..2031.00 rows=55564 width=49) (actual rows=55598.00 loops=1)
+               Bloom Filter 1: keys=(id1a, id1b) expected=55.6% checked=100000 rejected=44402 (44.4%)
+         ->  Hash  (cost=222.18..222.18 rows=1000 width=52) (actual rows=1000.00 loops=1)
+               Buckets: 1024  Batches: 1  Memory Usage: 91kB
+               ->  Merge Join  (cost=170.18..222.18 rows=1000 width=52) (actual rows=1000.00 loops=1)
+                     Merge Cond: ((d22.a = d2.id22a) AND (d22.b = d2.id22b))
+                     ->  Index Scan using bloom_snowflake_multi_dim_2_2_pkey on bloom_snowflake_multi_dim_2_2 d22  (cost=0.29..395.16 rows=10000 width=12) (actual rows=1001.00 loops=1)
+                           Index Searches: 1
+                     ->  Sort  (cost=169.90..172.40 rows=1000 width=40) (actual rows=1000.00 loops=1)
+                           Sort Key: d2.id22a, d2.id22b
+                           Sort Method: quicksort  Memory: 79kB
+                           ->  Merge Join  (cost=68.16..120.07 rows=1000 width=40) (actual rows=1000.00 loops=1)
+                                 Merge Cond: ((d21.a = d2.id21a) AND (d21.b = d2.id21b))
+                                 ->  Index Scan using bloom_snowflake_multi_dim_2_1_pkey on bloom_snowflake_multi_dim_2_1 d21  (cost=0.29..395.16 rows=10000 width=12) (actual rows=1000.00 loops=1)
+                                       Index Searches: 1
+                                 ->  Sort  (cost=67.83..70.33 rows=1000 width=28) (actual rows=1000.00 loops=1)
+                                       Sort Key: d2.id21a, d2.id21b
+                                       Sort Method: quicksort  Memory: 71kB
+                                       ->  Seq Scan on bloom_snowflake_multi_dim_2 d2  (cost=0.00..18.00 rows=1000 width=28) (actual rows=1000.00 loops=1)
+   ->  Hash  (cost=86.47..86.47 rows=556 width=52) (actual rows=555.00 loops=1)
+         Buckets: 1024  Batches: 1  Memory Usage: 54kB
+         Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=100000 rejected=44402
+         ->  Hash Join  (cost=59.36..86.47 rows=556 width=52) (actual rows=555.00 loops=1)
+               Hash Cond: ((d1.id12a = d12.a) AND (d1.id12b = d12.b))
+               ->  Hash Join  (cost=29.51..52.76 rows=734 width=40) (actual rows=737.00 loops=1)
+                     Hash Cond: ((d1.id11a = d11.a) AND (d1.id11b = d11.b))
+                     ->  Seq Scan on bloom_snowflake_multi_dim_1 d1  (cost=0.00..18.00 rows=1000 width=28) (actual rows=1000.00 loops=1)
+                     ->  Hash  (cost=18.50..18.50 rows=734 width=12) (actual rows=735.00 loops=1)
+                           Buckets: 1024  Batches: 1  Memory Usage: 40kB
+                           ->  Seq Scan on bloom_snowflake_multi_dim_1_1 d11  (cost=0.00..18.50 rows=734 width=12) (actual rows=735.00 loops=1)
+                                 Filter: (r < '0.75'::double precision)
+                                 Rows Removed by Filter: 265
+               ->  Hash  (cost=18.50..18.50 rows=757 width=12) (actual rows=758.00 loops=1)
+                     Buckets: 1024  Batches: 1  Memory Usage: 41kB
+                     ->  Seq Scan on bloom_snowflake_multi_dim_1_2 d12  (cost=0.00..18.50 rows=757 width=12) (actual rows=758.00 loops=1)
+                           Filter: (r < '0.75'::double precision)
+                           Rows Removed by Filter: 242
+(41 rows)
+
+RESET bloom_filter_pushdown_max_build_relids;
+DROP TABLE bloom_snowflake_multi_fact;
+DROP TABLE bloom_snowflake_multi_dim_1;
+DROP TABLE bloom_snowflake_multi_dim_2;
+DROP TABLE bloom_snowflake_multi_dim_1_1;
+DROP TABLE bloom_snowflake_multi_dim_1_2;
+DROP TABLE bloom_snowflake_multi_dim_2_1;
+DROP TABLE bloom_snowflake_multi_dim_2_2;
diff --git a/src/test/regress/sql/hashjoin_bloom_snowflake.sql b/src/test/regress/sql/hashjoin_bloom_snowflake.sql
index e5dcb2dea05..d8119e1528f 100644
--- a/src/test/regress/sql/hashjoin_bloom_snowflake.sql
+++ b/src/test/regress/sql/hashjoin_bloom_snowflake.sql
@@ -137,3 +137,101 @@ DROP TABLE bloom_snowflake_dim_2_1;
 DROP TABLE bloom_snowflake_dim_2_2;
 DROP TABLE bloom_snowflake_dim_2;
 DROP TABLE bloom_snowflake_fact;
+
+
+
+-- snowflake queries on multi-column FK joins
+CREATE TABLE bloom_snowflake_multi_dim_1_1 (a int, b int, r real, primary key (a, b));
+CREATE TABLE bloom_snowflake_multi_dim_1_2 (a int, b int, r real, primary key (a, b));
+
+CREATE TABLE bloom_snowflake_multi_dim_1 (
+	a int, b int,
+	id11a int, id11b int,
+	id12a int, id12b int,
+	r real,
+	foreign key (id11a, id11b) references bloom_snowflake_multi_dim_1_1(a, b),
+	foreign key (id12a, id12b) references bloom_snowflake_multi_dim_1_2(a, b),
+	primary key (a, b));
+
+CREATE TABLE bloom_snowflake_multi_dim_2_1 (a int, b int, r real, primary key (a, b));
+CREATE TABLE bloom_snowflake_multi_dim_2_2 (a int, b int, r real, primary key (a, b));
+
+CREATE TABLE bloom_snowflake_multi_dim_2 (
+	a int, b int,
+	id21a int, id21b int,
+	id22a int, id22b int,
+	r real,
+	foreign key (id21a, id21b) references bloom_snowflake_multi_dim_2_1(a, b),
+	foreign key (id22a, id22b) references bloom_snowflake_multi_dim_2_2(a, b),
+	primary key (a, b));
+
+CREATE TABLE bloom_snowflake_multi_fact (
+	id1a int, id1b int,
+	id2a int, id2b int,
+	padding text,
+	foreign key (id1a, id1b) references bloom_snowflake_multi_dim_1(a, b),
+	foreign key (id2a, id2b) references bloom_snowflake_multi_dim_2(a, b));
+
+SELECT setseed(0.5);
+
+INSERT INTO bloom_snowflake_multi_dim_1_1 SELECT i, i, random() FROM generate_series(1, 1000) s(i);
+INSERT INTO bloom_snowflake_multi_dim_1_2 SELECT i, i, random() FROM generate_series(1, 1000) s(i);
+
+WITH d AS (SELECT i, 1 + mod((100000 * random())::int, 1000) AS a, 1 + mod((100000 * random())::int, 1000) AS b FROM generate_series(1, 1000) s(i))
+INSERT INTO bloom_snowflake_multi_dim_1 SELECT i, i, a, a, b, b, random() FROM d;
+
+INSERT INTO bloom_snowflake_multi_dim_2_1 SELECT i, i, random() FROM generate_series(1, 10000) s(i);
+INSERT INTO bloom_snowflake_multi_dim_2_2 SELECT i, i, random() FROM generate_series(1, 10000) s(i);
+
+WITH d AS (SELECT i, 1 + mod((100000 * random())::int, 1000) AS a, 1 + mod((100000 * random())::int, 1000) AS b FROM generate_series(1, 1000) s(i))
+INSERT INTO bloom_snowflake_multi_dim_2 SELECT i, i, a, a, b, b, random() FROM d;
+
+WITH d AS (SELECT 1 + mod((100000 * random())::int, 1000) AS a, 1 + mod((100000 * random())::int, 1000) AS b, md5(i::text) AS p FROM generate_series(1, 100000) AS s(i))
+INSERT INTO bloom_snowflake_multi_fact
+SELECT
+    a, a, b, b, p
+FROM d;
+
+SET default_statistics_target = 1000;
+
+VACUUM ANALYZE;
+
+-- increase the accepted build size (includes the fact)
+SET bloom_filter_pushdown_max_build_relids = 4;
+
+EXPLAIN (ANALYZE, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
+SELECT *
+FROM bloom_snowflake_multi_fact f
+JOIN bloom_snowflake_multi_dim_1 d1 ON (f.id1a = d1.a AND f.id1b = d1.b)
+JOIN bloom_snowflake_multi_dim_1_1 d11 ON (d1.id11a = d11.a AND d1.id11b = d11.b)
+JOIN bloom_snowflake_multi_dim_1_2 d12 ON (d1.id12a = d12.a AND d1.id12b = d12.b)
+WHERE d11.r < 0.45 AND d12.r < 0.55;
+
+EXPLAIN (ANALYZE, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
+SELECT *
+FROM bloom_snowflake_multi_fact f
+JOIN bloom_snowflake_multi_dim_1 d1 ON (f.id1a = d1.a AND f.id1b = d1.b)
+JOIN bloom_snowflake_multi_dim_1_1 d11 ON (d1.id11a = d11.a AND d1.id11b = d11.b)
+JOIN bloom_snowflake_multi_dim_1_2 d12 ON (d1.id12a = d12.a AND d1.id12b = d12.b)
+WHERE d11.r < 0.75 AND d12.r < 0.75;
+
+EXPLAIN (ANALYZE, TIMING OFF, SUMMARY OFF, BUFFERS OFF)
+SELECT *
+FROM bloom_snowflake_multi_fact f
+JOIN bloom_snowflake_multi_dim_1 d1 ON (f.id1a = d1.a AND f.id1b = d1.b)
+JOIN bloom_snowflake_multi_dim_1_1 d11 ON (d1.id11a = d11.a AND d1.id11b = d11.b)
+JOIN bloom_snowflake_multi_dim_1_2 d12 ON (d1.id12a = d12.a AND d1.id12b = d12.b)
+JOIN bloom_snowflake_multi_dim_2 d2 ON (f.id2a = d2.a AND f.id2b = d2.b)
+JOIN bloom_snowflake_multi_dim_2_1 d21 ON (d2.id21a = d21.a AND d2.id21b = d21.b)
+JOIN bloom_snowflake_multi_dim_2_2 d22 ON (d2.id22a = d22.a AND d2.id22b = d22.b)
+WHERE d11.r < 0.75 AND d12.r < 0.75;
+
+RESET bloom_filter_pushdown_max_build_relids;
+
+DROP TABLE bloom_snowflake_multi_fact;
+DROP TABLE bloom_snowflake_multi_dim_1;
+DROP TABLE bloom_snowflake_multi_dim_2;
+DROP TABLE bloom_snowflake_multi_dim_1_1;
+DROP TABLE bloom_snowflake_multi_dim_1_2;
+DROP TABLE bloom_snowflake_multi_dim_2_1;
+DROP TABLE bloom_snowflake_multi_dim_2_2;
-- 
2.50.1 (Apple Git-155)

