From 6bbd8ba68e32f745a7ec636affbf83ad4b8b3ab9 Mon Sep 17 00:00:00 2001
From: Tomas Vondra <tomas@vondra.me>
Date: Sat, 1 Aug 2026 15:14:41 +0200
Subject: [PATCH v10 19/21] Size filters for cheap probes

Size hashjoin filters for cheap probes. The filters were built with
bloom_create(), whose sizing favors accuracy: a 1MB bitset floor and
up to MAX_HASH_FUNCS (10) hash functions.  That is wrong for a filter
that probes a large apply side.  A filter probing tens of millions of
rows then does ~10 random accesses into a bitset that does not fit in
L2, so each probe is effectively a series of cache misses -- it is no
longer the cheap constant-k operation the cost model assumes, and the
filter can cost more than it saves even while rejecting most of the
rows.

Add bloom_create_probe() alongside bloom_create() (both share a static
bloom_create_internal(), differing only in the bitset floor and the
hash-function cap).  Callers that need accuracy keep the 1MB / 10-hash
sizing; the hashjoin filters in nodeHash.c use bloom_create_probe(),
which uses a 1KB floor and caps the hash functions at 4.  The filter
is then sized to the build side and stays cache-resident, so the
per-tuple probe is genuinely cheap.
---
 src/backend/executor/nodeHash.c               | 53 +++++++++++--
 src/backend/lib/bloomfilter.c                 | 16 +++-
 src/include/lib/bloomfilter.h                 |  4 +
 src/test/regress/expected/hashjoin_bloom.out  |  6 +-
 .../expected/hashjoin_bloom_snowflake.out     | 26 +++----
 .../regress/expected/hashjoin_bloom_star.out  | 74 +++++++++----------
 6 files changed, 117 insertions(+), 62 deletions(-)

diff --git a/src/backend/executor/nodeHash.c b/src/backend/executor/nodeHash.c
index e68bcccea69..ca6e5662b1f 100644
--- a/src/backend/executor/nodeHash.c
+++ b/src/backend/executor/nodeHash.c
@@ -82,6 +82,17 @@ static bool ExecParallelHashTuplePrealloc(HashJoinTable hashtable,
 static void ExecParallelHashMergeCounters(HashJoinTable hashtable);
 static void ExecParallelHashCloseBatchAccessors(HashJoinTable hashtable);
 
+static bloom_filter *bloom_create_probe(int64 total_elems, int bloom_work_mem, uint64 seed);
+
+
+/*
+ * Sizing for probe-oriented Bloom filters. A small bitset floor, so small
+ * build sides stay resident in CPU cache, and a low cap on the number of hash
+ * functions, since each is an independent (potentially cache-missing) bitset
+ * access on every probe.
+ */
+#define BLOOM_PROBE_MIN_BITSET_BYTES	1024
+#define BLOOM_PROBE_MAX_HASH_FUNCS		4
 
 /* ----------------------------------------------------------------
  *		ExecHash
@@ -714,8 +725,10 @@ ExecHashTableCreate(HashState *state)
 	 * same lifecycle (during rescans, etc.).
 	 *
 	 * The size of the filter is bounded by both the estimated inner row count
-	 * and a fixed fraction of work_mem.  bloom_create() will round down to
-	 * the next power-of-two bitset and enforces a 1MB minimum.
+	 * and a fixed fraction of work_mem.  We use bloom_create_probe(), which
+	 * (unlike bloom_create) sizes the bitset to the build side without a 1MB
+	 * floor and caps the hash-function count, so the filter stays small
+	 * enough to be cache-resident and cheap to probe per tuple.
 	 *
 	 * XXX This may need more thought. If we limit bloom_work_mem too much,
 	 * the false positive rate will get too bad, and we won't filter enough
@@ -746,8 +759,8 @@ ExecHashTableCreate(HashState *state)
 		bloom_work_mem = Max(1024, work_mem / 8);
 
 		oldctx = MemoryContextSwitchTo(hashtable->hashCxt);
-		state->bloom_filter = bloom_create((int64) Max(rows, 1.0),
-										   bloom_work_mem, 0);
+		state->bloom_filter = bloom_create_probe((int64) Max(rows, 1.0),
+												 bloom_work_mem, 0);
 
 		/*
 		 * If a recipient opted in, also build one filter per join key (in
@@ -760,8 +773,8 @@ ExecHashTableCreate(HashState *state)
 			state->perkey_filters = palloc_array(struct bloom_filter *,
 												 state->perkey_nfilters);
 			for (int i = 0; i < state->perkey_nfilters; i++)
-				state->perkey_filters[i] = bloom_create((int64) Max(rows, 1.0),
-														bloom_work_mem, 0);
+				state->perkey_filters[i] = bloom_create_probe((int64) Max(rows, 1.0),
+															  bloom_work_mem, 0);
 		}
 
 		MemoryContextSwitchTo(oldctx);
@@ -3791,3 +3804,31 @@ get_hash_memory_limit(void)
 
 	return (size_t) mem_limit;
 }
+
+/*
+ * Create a Bloom filter tuned for repeated per-tuple probing within a query,
+ * rather than one built once and checked in bulk.
+ *
+ * bloom_create's sizing is a poor fit for that use in two ways, both of which
+ * make each probe expensive:
+ *
+ * - Size.  bloom_create enforces a 1MB minimum bitset, which for a typical
+ *   build side does not fit in the CPU's L2 cache, so every probe of a large
+ *   scan incurs cache misses.  Here we drop that floor and let the bitset be
+ *   sized to the build side, so small build sides yield cache-resident filters.
+ *
+ * - Hash functions.  Each of the k hash functions is an independent random
+ *   access into the bitset, so a high k means many (cache-missing) accesses per
+ *   probe.  We cap k well below MAX_HASH_FUNCS, trading a slightly higher
+ *   false-positive rate for much cheaper probes.
+ *
+ * This mirrors the "small fixed number of hash functions" and "keep the filter
+ * within the L2 cache" choices in the bottom-up Bloom filter paper.
+ */
+bloom_filter *
+bloom_create_probe(int64 total_elems, int bloom_work_mem, uint64 seed)
+{
+	return bloom_create_custom(total_elems, bloom_work_mem,
+							   BLOOM_PROBE_MIN_BITSET_BYTES,
+							   BLOOM_PROBE_MAX_HASH_FUNCS, seed);
+}
diff --git a/src/backend/lib/bloomfilter.c b/src/backend/lib/bloomfilter.c
index fbcf788e271..9f2f9006ab7 100644
--- a/src/backend/lib/bloomfilter.c
+++ b/src/backend/lib/bloomfilter.c
@@ -84,7 +84,8 @@ static inline uint32 mod_m(uint32 val, uint64 m);
  * 0.  Callers can also use a pseudo-random seed, eg from pg_prng_uint64().
  */
 bloom_filter *
-bloom_create(int64 total_elems, int bloom_work_mem, uint64 seed)
+bloom_create_custom(int64 total_elems, int bloom_work_mem,
+					  uint64 min_bitset_bytes, int max_hash_funcs, uint64 seed)
 {
 	bloom_filter *filter;
 	int			bloom_power;
@@ -99,7 +100,7 @@ bloom_create(int64 total_elems, int bloom_work_mem, uint64 seed)
 	 * false positive rate still won't exceed 2% in almost all cases.
 	 */
 	bitset_bytes = Min(bloom_work_mem * UINT64CONST(1024), total_elems * 2);
-	bitset_bytes = Max(1024 * 1024, bitset_bytes);
+	bitset_bytes = Max(min_bitset_bytes, bitset_bytes);
 
 	/*
 	 * Size in bits should be the highest power of two <= target.  bitset_bits
@@ -112,13 +113,22 @@ bloom_create(int64 total_elems, int bloom_work_mem, uint64 seed)
 	/* Allocate bloom filter with unset bitset */
 	filter = palloc0(offsetof(bloom_filter, bitset) +
 					 sizeof(unsigned char) * bitset_bytes);
-	filter->k_hash_funcs = optimal_k(bitset_bits, total_elems);
+	filter->k_hash_funcs = Min(optimal_k(bitset_bits, total_elems),
+							   max_hash_funcs);
 	filter->seed = seed;
 	filter->m = bitset_bits;
 
 	return filter;
 }
 
+bloom_filter *
+bloom_create(int64 total_elems, int bloom_work_mem, uint64 seed)
+{
+	return bloom_create_custom(total_elems, bloom_work_mem,
+								 UINT64CONST(1024) * 1024, MAX_HASH_FUNCS,
+								 seed);
+}
+
 /*
  * Free Bloom filter
  */
diff --git a/src/include/lib/bloomfilter.h b/src/include/lib/bloomfilter.h
index 9277087acb6..9a79e83e7d9 100644
--- a/src/include/lib/bloomfilter.h
+++ b/src/include/lib/bloomfilter.h
@@ -17,6 +17,10 @@ typedef struct bloom_filter bloom_filter;
 
 extern bloom_filter *bloom_create(int64 total_elems, int bloom_work_mem,
 								  uint64 seed);
+extern bloom_filter *bloom_create_custom(int64 total_elems, int bloom_work_mem,
+										 uint64 min_bitset_bytes,
+										 int max_hash_funcs,
+										 uint64 seed);
 extern void bloom_free(bloom_filter *filter);
 extern void bloom_add_element(bloom_filter *filter, unsigned char *elem,
 							  size_t len);
diff --git a/src/test/regress/expected/hashjoin_bloom.out b/src/test/regress/expected/hashjoin_bloom.out
index 23be0a2bcc8..36a7f95fe42 100644
--- a/src/test/regress/expected/hashjoin_bloom.out
+++ b/src/test/regress/expected/hashjoin_bloom.out
@@ -117,11 +117,11 @@ WHERE d.r < 0.5;
 --------------------------------------------------------------------------------------------------------------
  Hash Join  (cost=25.97..685.41 rows=14940 width=53) (actual rows=14970.00 loops=1)
    Hash Cond: ((f.id1 = d.id1) AND (f.id2 = d.id2))
-   ->  Seq Scan on bloom_multi_fact f  (cost=0.00..581.00 rows=14940 width=41) (actual rows=14970.00 loops=1)
-         Bloom Filter 1: keys=(id1, id2) expected=49.8% checked=29999 rejected=15030 (50.1%)
+   ->  Seq Scan on bloom_multi_fact f  (cost=0.00..581.00 rows=14940 width=41) (actual rows=15000.00 loops=1)
+         Bloom Filter 1: keys=(id1, id2) expected=49.8% checked=29999 rejected=15000 (50.0%)
    ->  Hash  (cost=18.50..18.50 rows=498 width=12) (actual rows=499.00 loops=1)
          Buckets: 1024  Batches: 1  Memory Usage: 30kB
-         Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=29999 rejected=15030
+         Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=29999 rejected=15000
          ->  Seq Scan on bloom_multi_dim d  (cost=0.00..18.50 rows=498 width=12) (actual rows=499.00 loops=1)
                Filter: (r < '0.5'::double precision)
                Rows Removed by Filter: 501
diff --git a/src/test/regress/expected/hashjoin_bloom_snowflake.out b/src/test/regress/expected/hashjoin_bloom_snowflake.out
index 1751df4c355..1f0e5b8abdb 100644
--- a/src/test/regress/expected/hashjoin_bloom_snowflake.out
+++ b/src/test/regress/expected/hashjoin_bloom_snowflake.out
@@ -438,23 +438,23 @@ WHERE d11.r < 0.45 AND d12.r < 0.55;
          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
+         Bloom Filter 3: bits=8192 hashes=4 memory=1kB 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 Join  (cost=25.09..44.38 rows=246 width=40) (actual rows=235.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)
+                     ->  Seq Scan on bloom_snowflake_multi_dim_1 d1  (cost=0.00..18.00 rows=246 width=28) (actual rows=236.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%)
+                           Bloom Filter 2: keys=(id12a, id12b) expected=56.1% checked=431 rejected=195 (45.2%)
                      ->  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
+                           Bloom Filter 1: bits=8192 hashes=4 memory=1kB 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
+                     Bloom Filter 2: bits=8192 hashes=4 memory=1kB checked=431 rejected=195
                      ->  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
@@ -471,11 +471,11 @@ WHERE d11.r < 0.75 AND d12.r < 0.75;
 ------------------------------------------------------------------------------------------------------------------------------------------------
  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%)
+   ->  Seq Scan on bloom_snowflake_multi_fact f  (cost=0.00..2031.00 rows=55564 width=49) (actual rows=55694.00 loops=1)
+         Bloom Filter 1: keys=(id1a, id1b) expected=55.6% checked=99999 rejected=44306 (44.3%)
    ->  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
+         Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=99999 rejected=44306
          ->  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)
@@ -507,10 +507,10 @@ WHERE d11.r < 0.75 AND d12.r < 0.75;
 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  Hash Join  (cost=331.90..3197.23 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.09..2685.38 rows=55564 width=101) (actual rows=55598.00 loops=1)
+   ->  Hash Join  (cost=237.09..2685.38 rows=55564 width=101) (actual rows=55694.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%)
+         ->  Seq Scan on bloom_snowflake_multi_fact f  (cost=0.00..2031.00 rows=55564 width=49) (actual rows=55694.00 loops=1)
+               Bloom Filter 1: keys=(id1a, id1b) expected=55.6% checked=100000 rejected=44306 (44.3%)
          ->  Hash  (cost=222.09..222.09 rows=1000 width=52) (actual rows=1000.00 loops=1)
                Buckets: 1024  Batches: 1  Memory Usage: 91kB
                ->  Merge Join  (cost=170.14..222.09 rows=1000 width=52) (actual rows=1000.00 loops=1)
@@ -530,7 +530,7 @@ WHERE d11.r < 0.75 AND d12.r < 0.75;
                                        ->  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
+         Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=100000 rejected=44306
          ->  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)
diff --git a/src/test/regress/expected/hashjoin_bloom_star.out b/src/test/regress/expected/hashjoin_bloom_star.out
index ad718a05a7c..ab73bf7b004 100644
--- a/src/test/regress/expected/hashjoin_bloom_star.out
+++ b/src/test/regress/expected/hashjoin_bloom_star.out
@@ -501,7 +501,7 @@ WHERE d1.r < 0.5;
                                              Bloom Filter 1: keys=(id1a, id1b) expected=46.9% checked=99999 rejected=53152 (53.2%)
                                        ->  Hash  (cost=18.50..18.50 rows=469 width=12) (actual rows=470.00 loops=1)
                                              Buckets: 1024  Batches: 1  Memory Usage: 29kB
-                                             Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=99999 rejected=53152
+                                             Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=99999 rejected=53152
                                              ->  Seq Scan on bloom_star_multi_dim_1 d1  (cost=0.00..18.50 rows=469 width=12) (actual rows=470.00 loops=1)
                                                    Filter: (r < '0.5'::double precision)
                                                    Rows Removed by Filter: 530
@@ -541,24 +541,24 @@ WHERE d1.r < 0.4 AND d7.r < 0.5;
 ----------------------------------------------------------------------------------------------------------------------------------------------------------
  Hash Join  (cost=204.78..3401.16 rows=17888 width=173) (actual rows=17864.00 loops=1)
    Hash Cond: ((f.id7a = d7.a) AND (f.id7b = d7.b))
-   ->  Hash Join  (cost=179.12..3281.60 rows=17888 width=161) (actual rows=17864.00 loops=1)
+   ->  Hash Join  (cost=179.12..3281.60 rows=17888 width=161) (actual rows=17899.00 loops=1)
          Hash Cond: ((f.id6a = d6.a) AND (f.id6b = d6.b))
-         ->  Hash Join  (cost=148.12..3156.69 rows=17888 width=149) (actual rows=17864.00 loops=1)
+         ->  Hash Join  (cost=148.12..3156.69 rows=17888 width=149) (actual rows=17899.00 loops=1)
                Hash Cond: ((f.id5a = d5.a) AND (f.id5b = d5.b))
-               ->  Hash Join  (cost=117.12..3031.77 rows=17888 width=137) (actual rows=17864.00 loops=1)
+               ->  Hash Join  (cost=117.12..3031.77 rows=17888 width=137) (actual rows=17899.00 loops=1)
                      Hash Cond: ((f.id4a = d4.a) AND (f.id4b = d4.b))
-                     ->  Hash Join  (cost=86.12..2906.86 rows=17888 width=125) (actual rows=17864.00 loops=1)
+                     ->  Hash Join  (cost=86.12..2906.86 rows=17888 width=125) (actual rows=17899.00 loops=1)
                            Hash Cond: ((f.id3a = d3.a) AND (f.id3b = d3.b))
-                           ->  Hash Join  (cost=55.12..2781.95 rows=17888 width=113) (actual rows=17864.00 loops=1)
+                           ->  Hash Join  (cost=55.12..2781.95 rows=17888 width=113) (actual rows=17899.00 loops=1)
                                  Hash Cond: ((f.id2a = d2.a) AND (f.id2b = d2.b))
-                                 ->  Hash Join  (cost=24.12..2657.04 rows=17888 width=101) (actual rows=17864.00 loops=1)
+                                 ->  Hash Join  (cost=24.12..2657.04 rows=17888 width=101) (actual rows=17899.00 loops=1)
                                        Hash Cond: ((f.id1a = d1.a) AND (f.id1b = d1.b))
-                                       ->  Seq Scan on bloom_star_multi_fact f  (cost=0.00..2539.00 rows=17888 width=89) (actual rows=17864.00 loops=1)
+                                       ->  Seq Scan on bloom_star_multi_fact f  (cost=0.00..2539.00 rows=17888 width=89) (actual rows=17899.00 loops=1)
                                              Bloom Filter 1: keys=(id1a, id1b) expected=37.5% checked=99999 rejected=62610 (62.6%)
-                                             Bloom Filter 2: keys=(id7a, id7b) expected=47.7% checked=37390 rejected=19526 (52.2%)
+                                             Bloom Filter 2: keys=(id7a, id7b) expected=47.7% checked=37390 rejected=19491 (52.1%)
                                        ->  Hash  (cost=18.50..18.50 rows=375 width=12) (actual rows=376.00 loops=1)
                                              Buckets: 1024  Batches: 1  Memory Usage: 25kB
-                                             Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=99999 rejected=62610
+                                             Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=99999 rejected=62610
                                              ->  Seq Scan on bloom_star_multi_dim_1 d1  (cost=0.00..18.50 rows=375 width=12) (actual rows=376.00 loops=1)
                                                    Filter: (r < '0.4'::double precision)
                                                    Rows Removed by Filter: 624
@@ -579,7 +579,7 @@ WHERE d1.r < 0.4 AND d7.r < 0.5;
                ->  Seq Scan on bloom_star_multi_dim_6 d6  (cost=0.00..16.00 rows=1000 width=12) (actual rows=1000.00 loops=1)
    ->  Hash  (cost=18.50..18.50 rows=477 width=12) (actual rows=478.00 loops=1)
          Buckets: 1024  Batches: 1  Memory Usage: 29kB
-         Bloom Filter 2: bits=8388608 hashes=10 memory=1024kB checked=37390 rejected=19526
+         Bloom Filter 2: bits=8192 hashes=4 memory=1kB checked=37390 rejected=19491
          ->  Seq Scan on bloom_star_multi_dim_7 d7  (cost=0.00..18.50 rows=477 width=12) (actual rows=478.00 loops=1)
                Filter: (r < '0.5'::double precision)
                Rows Removed by Filter: 522
@@ -609,29 +609,29 @@ WHERE d1.r < 0.3 AND d2.r < 0.35 AND d3.r < 0.4 AND d4.r < 0.45 AND d5.r < 0.5 A
                      Hash Cond: ((f.id4a = d4.a) AND (f.id4b = d4.b))
                      ->  Hash Join  (cost=71.02..2673.53 rows=4032 width=125) (actual rows=3997.00 loops=1)
                            Hash Cond: ((f.id3a = d3.a) AND (f.id3b = d3.b))
-                           ->  Hash Join  (cost=46.69..2628.03 rows=4032 width=113) (actual rows=3997.00 loops=1)
+                           ->  Hash Join  (cost=46.69..2628.03 rows=4032 width=113) (actual rows=4005.00 loops=1)
                                  Hash Cond: ((f.id2a = d2.a) AND (f.id2b = d2.b))
-                                 ->  Hash Join  (cost=22.95..2583.12 rows=4032 width=101) (actual rows=3997.00 loops=1)
+                                 ->  Hash Join  (cost=22.95..2583.12 rows=4032 width=101) (actual rows=4016.00 loops=1)
                                        Hash Cond: ((f.id1a = d1.a) AND (f.id1b = d1.b))
-                                       ->  Seq Scan on bloom_star_multi_fact f  (cost=0.00..2539.00 rows=4032 width=89) (actual rows=3997.00 loops=1)
+                                       ->  Seq Scan on bloom_star_multi_fact f  (cost=0.00..2539.00 rows=4032 width=89) (actual rows=4016.00 loops=1)
                                              Bloom Filter 1: keys=(id1a, id1b) expected=29.7% checked=99991 rejected=70413 (70.4%)
-                                             Bloom Filter 2: keys=(id2a, id2b) expected=34.9% checked=29587 rejected=19399 (65.6%)
-                                             Bloom Filter 3: keys=(id3a, id3b) expected=38.9% checked=10188 rejected=6191 (60.8%)
+                                             Bloom Filter 2: keys=(id2a, id2b) expected=34.9% checked=29587 rejected=19372 (65.5%)
+                                             Bloom Filter 3: keys=(id3a, id3b) expected=38.9% checked=10215 rejected=6199 (60.7%)
                                        ->  Hash  (cost=18.50..18.50 rows=297 width=12) (actual rows=298.00 loops=1)
                                              Buckets: 1024  Batches: 1  Memory Usage: 21kB
-                                             Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=99991 rejected=70413
+                                             Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=99991 rejected=70413
                                              ->  Seq Scan on bloom_star_multi_dim_1 d1  (cost=0.00..18.50 rows=297 width=12) (actual rows=298.00 loops=1)
                                                    Filter: (r < '0.3'::double precision)
                                                    Rows Removed by Filter: 702
                                  ->  Hash  (cost=18.50..18.50 rows=349 width=12) (actual rows=350.00 loops=1)
                                        Buckets: 1024  Batches: 1  Memory Usage: 24kB
-                                       Bloom Filter 2: bits=8388608 hashes=10 memory=1024kB checked=29587 rejected=19399
+                                       Bloom Filter 2: bits=8192 hashes=4 memory=1kB checked=29587 rejected=19372
                                        ->  Seq Scan on bloom_star_multi_dim_2 d2  (cost=0.00..18.50 rows=349 width=12) (actual rows=350.00 loops=1)
                                              Filter: (r < '0.35'::double precision)
                                              Rows Removed by Filter: 650
                            ->  Hash  (cost=18.50..18.50 rows=389 width=12) (actual rows=391.00 loops=1)
                                  Buckets: 1024  Batches: 1  Memory Usage: 25kB
-                                 Bloom Filter 3: bits=8388608 hashes=10 memory=1024kB checked=10188 rejected=6191
+                                 Bloom Filter 3: bits=8192 hashes=4 memory=1kB checked=10215 rejected=6199
                                  ->  Seq Scan on bloom_star_multi_dim_3 d3  (cost=0.00..18.50 rows=389 width=12) (actual rows=391.00 loops=1)
                                        Filter: (r < '0.4'::double precision)
                                        Rows Removed by Filter: 609
@@ -678,61 +678,61 @@ WHERE d1.r < 0.3 AND d2.r < 0.35 AND d3.r < 0.4 AND d4.r < 0.45 AND d5.r < 0.5 A
          Hash Cond: ((f.id6a = d6.a) AND (f.id6b = d6.b))
          ->  Hash Join  (cost=122.51..2668.94 rows=283 width=149) (actual rows=292.00 loops=1)
                Hash Cond: ((f.id5a = d5.a) AND (f.id5b = d5.b))
-               ->  Hash Join  (cost=96.24..2641.19 rows=283 width=137) (actual rows=292.00 loops=1)
+               ->  Hash Join  (cost=96.24..2641.19 rows=283 width=137) (actual rows=296.00 loops=1)
                      Hash Cond: ((f.id4a = d4.a) AND (f.id4b = d4.b))
-                     ->  Hash Join  (cost=71.02..2614.48 rows=283 width=125) (actual rows=292.00 loops=1)
+                     ->  Hash Join  (cost=71.02..2614.48 rows=283 width=125) (actual rows=296.00 loops=1)
                            Hash Cond: ((f.id3a = d3.a) AND (f.id3b = d3.b))
-                           ->  Hash Join  (cost=46.69..2588.66 rows=283 width=113) (actual rows=292.00 loops=1)
+                           ->  Hash Join  (cost=46.69..2588.66 rows=283 width=113) (actual rows=298.00 loops=1)
                                  Hash Cond: ((f.id2a = d2.a) AND (f.id2b = d2.b))
-                                 ->  Hash Join  (cost=22.95..2563.44 rows=283 width=101) (actual rows=292.00 loops=1)
+                                 ->  Hash Join  (cost=22.95..2563.44 rows=283 width=101) (actual rows=299.00 loops=1)
                                        Hash Cond: ((f.id1a = d1.a) AND (f.id1b = d1.b))
-                                       ->  Seq Scan on bloom_star_multi_fact f  (cost=0.00..2539.00 rows=283 width=89) (actual rows=292.00 loops=1)
+                                       ->  Seq Scan on bloom_star_multi_fact f  (cost=0.00..2539.00 rows=283 width=89) (actual rows=299.00 loops=1)
                                              Bloom Filter 1: keys=(id1a, id1b) expected=29.7% checked=99991 rejected=70413 (70.4%)
-                                             Bloom Filter 2: keys=(id2a, id2b) expected=34.9% checked=29587 rejected=19399 (65.6%)
-                                             Bloom Filter 3: keys=(id3a, id3b) expected=38.9% checked=10188 rejected=6191 (60.8%)
-                                             Bloom Filter 4: keys=(id4a, id4b) expected=44.8% checked=3997 rejected=2223 (55.6%)
-                                             Bloom Filter 5: keys=(id5a, id5b) expected=51.8% checked=1774 rejected=851 (48.0%)
-                                             Bloom Filter 6: keys=(id6a, id6b) expected=53.7% checked=923 rejected=397 (43.0%)
-                                             Bloom Filter 7: keys=(id7a, id7b) expected=56.3% checked=526 rejected=234 (44.5%)
+                                             Bloom Filter 2: keys=(id2a, id2b) expected=34.9% checked=29587 rejected=19372 (65.5%)
+                                             Bloom Filter 3: keys=(id3a, id3b) expected=38.9% checked=10215 rejected=6199 (60.7%)
+                                             Bloom Filter 4: keys=(id4a, id4b) expected=44.8% checked=4016 rejected=2234 (55.6%)
+                                             Bloom Filter 5: keys=(id5a, id5b) expected=51.8% checked=1782 rejected=848 (47.6%)
+                                             Bloom Filter 6: keys=(id6a, id6b) expected=53.7% checked=934 rejected=399 (42.7%)
+                                             Bloom Filter 7: keys=(id7a, id7b) expected=56.3% checked=535 rejected=236 (44.1%)
                                        ->  Hash  (cost=18.50..18.50 rows=297 width=12) (actual rows=298.00 loops=1)
                                              Buckets: 1024  Batches: 1  Memory Usage: 21kB
-                                             Bloom Filter 1: bits=8388608 hashes=10 memory=1024kB checked=99991 rejected=70413
+                                             Bloom Filter 1: bits=8192 hashes=4 memory=1kB checked=99991 rejected=70413
                                              ->  Seq Scan on bloom_star_multi_dim_1 d1  (cost=0.00..18.50 rows=297 width=12) (actual rows=298.00 loops=1)
                                                    Filter: (r < '0.3'::double precision)
                                                    Rows Removed by Filter: 702
                                  ->  Hash  (cost=18.50..18.50 rows=349 width=12) (actual rows=350.00 loops=1)
                                        Buckets: 1024  Batches: 1  Memory Usage: 24kB
-                                       Bloom Filter 2: bits=8388608 hashes=10 memory=1024kB checked=29587 rejected=19399
+                                       Bloom Filter 2: bits=8192 hashes=4 memory=1kB checked=29587 rejected=19372
                                        ->  Seq Scan on bloom_star_multi_dim_2 d2  (cost=0.00..18.50 rows=349 width=12) (actual rows=350.00 loops=1)
                                              Filter: (r < '0.35'::double precision)
                                              Rows Removed by Filter: 650
                            ->  Hash  (cost=18.50..18.50 rows=389 width=12) (actual rows=391.00 loops=1)
                                  Buckets: 1024  Batches: 1  Memory Usage: 25kB
-                                 Bloom Filter 3: bits=8388608 hashes=10 memory=1024kB checked=10188 rejected=6191
+                                 Bloom Filter 3: bits=8192 hashes=4 memory=1kB checked=10215 rejected=6199
                                  ->  Seq Scan on bloom_star_multi_dim_3 d3  (cost=0.00..18.50 rows=389 width=12) (actual rows=391.00 loops=1)
                                        Filter: (r < '0.4'::double precision)
                                        Rows Removed by Filter: 609
                      ->  Hash  (cost=18.50..18.50 rows=448 width=12) (actual rows=449.00 loops=1)
                            Buckets: 1024  Batches: 1  Memory Usage: 28kB
-                           Bloom Filter 4: bits=8388608 hashes=10 memory=1024kB checked=3997 rejected=2223
+                           Bloom Filter 4: bits=8192 hashes=4 memory=1kB checked=4016 rejected=2234
                            ->  Seq Scan on bloom_star_multi_dim_4 d4  (cost=0.00..18.50 rows=448 width=12) (actual rows=449.00 loops=1)
                                  Filter: (r < '0.45'::double precision)
                                  Rows Removed by Filter: 551
                ->  Hash  (cost=18.50..18.50 rows=518 width=12) (actual rows=519.00 loops=1)
                      Buckets: 1024  Batches: 1  Memory Usage: 31kB
-                     Bloom Filter 5: bits=8388608 hashes=10 memory=1024kB checked=1774 rejected=851
+                     Bloom Filter 5: bits=8192 hashes=4 memory=1kB checked=1782 rejected=848
                      ->  Seq Scan on bloom_star_multi_dim_5 d5  (cost=0.00..18.50 rows=518 width=12) (actual rows=519.00 loops=1)
                            Filter: (r < '0.5'::double precision)
                            Rows Removed by Filter: 481
          ->  Hash  (cost=18.50..18.50 rows=537 width=12) (actual rows=538.00 loops=1)
                Buckets: 1024  Batches: 1  Memory Usage: 32kB
-               Bloom Filter 6: bits=8388608 hashes=10 memory=1024kB checked=923 rejected=397
+               Bloom Filter 6: bits=8192 hashes=4 memory=1kB checked=934 rejected=399
                ->  Seq Scan on bloom_star_multi_dim_6 d6  (cost=0.00..18.50 rows=537 width=12) (actual rows=538.00 loops=1)
                      Filter: (r < '0.55'::double precision)
                      Rows Removed by Filter: 462
    ->  Hash  (cost=18.50..18.50 rows=563 width=12) (actual rows=564.00 loops=1)
          Buckets: 1024  Batches: 1  Memory Usage: 33kB
-         Bloom Filter 7: bits=8388608 hashes=10 memory=1024kB checked=526 rejected=234
+         Bloom Filter 7: bits=8192 hashes=4 memory=1kB checked=535 rejected=236
          ->  Seq Scan on bloom_star_multi_dim_7 d7  (cost=0.00..18.50 rows=563 width=12) (actual rows=564.00 loops=1)
                Filter: (r < '0.6'::double precision)
                Rows Removed by Filter: 436
-- 
2.50.1 (Apple Git-155)

