Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wckh9-003b77-2p for pgsql-hackers@arkaria.postgresql.org; Thu, 25 Jun 2026 14:04:44 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wckh7-006FHZ-1R for pgsql-hackers@arkaria.postgresql.org; Thu, 25 Jun 2026 14:04:41 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wckh7-006FHR-0T for pgsql-hackers@lists.postgresql.org; Thu, 25 Jun 2026 14:04:41 +0000 Received: from relay4-d.mail.gandi.net ([2001:4b98:dc4:8::224]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wckh4-00000000CyZ-38LR for pgsql-hackers@lists.postgresql.org; Thu, 25 Jun 2026 14:04:40 +0000 Received: by mail.gandi.net (Postfix) with ESMTPSA id 5BBCB3F685; Thu, 25 Jun 2026 14:04:32 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vondra.me; s=gm1; t=1782396272; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=y7eWs+AEyPAKPPdzkk7ERw2ywXEbs6jXgvlyynnpJSw=; b=MfGOCcCu3Jotuw4QDifaleGj2ZdFijxiCCFPMPWzKteFU+FpSA1YfZdTOWRfCAgo/ldm6C lQgwS97Nm+HTo0J6chU2MGLZMG6nViorATEXkhqpdrn0E1Lsk7oE8Y4t7qQ4Nq4scKBLJR N/3SZ3eLjYAbvUUA8kQtMPJZbpc7dJ/FRAgve8SDwW7a1GA/fSs5kzqkzewd95mdvE7HT1 wssK0JWV5HNUK2b3+EAMg8wChSeyZIPByQxPTQ4zljshBm8oRxb5g6cmzUeuaWRc66XrOq BWhRtrbW7/0rNW7GegCbJGLpWxWMuiKIOoAgZE3hCzHg4qg+o7d4rx5PSv+KbA== Message-ID: Date: Thu, 25 Jun 2026 16:04:30 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: hashjoins vs. Bloom filters (yet again) To: Ben Mejia , PostgreSQL Hackers References: <5cd8c20c-14b5-4b0d-bedc-69bf714e87eb@vondra.me> <2277c338-87ee-424c-a03c-4b6f589ccf26@gmail.com> Content-Language: en-US From: Tomas Vondra In-Reply-To: <2277c338-87ee-424c-a03c-4b6f589ccf26@gmail.com> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-GND-Sasl: tomas@vondra.me X-GND-State: clean X-GND-Score: -100 X-GND-Cause: dmFkZTFGAz1m/RNRdWxGL59LIpTr0DlNFmSDqlxNxDoG4GIPsmWWd0+gCL7mKH7N9AE5+g3VmKkRt1+WzGk4qCU4MUDvjoki/f7BYBXDeWxPAhUjpqkq8vGy7u73CDgrzCcMLAjPgY7h0hajB5J65f4uZZ3BZex8E/ykpYmXLdruJCYzN73XpmQAndQHFCuavnNQIWfpfSGz/yftF///WcYJGeZJqRBLYCFZaXz4h70C0rFggJVn93FqCXNaqm+rCTRy5qYyUh6QO3ZDdNRCoZUVLh2OMjzXt69zKb3U2qPebGhiaOmmRjmv4bJTt6vaK26U4dG2ne1tb3uY1smFWXaNouNNqLHXhQVsGl/cVxNA826dzFu/Zw3lmEgUcSMTeXbPah7jmu6j6l7pRnF6bONeBtRb7yjh1P2PZz8fik01ltW0wsGejJZAiBtOokIMYxdaAz4QaA+aiSt62V3ZSgEMCPVLixecVi4rdvjwztEVZ0jw7HEqes7xsEHytRiIES8hUlhx8oxB4G8vtcVMNE9GwcG6d35A69zXg92fFInZC7hTI/pghwO6SNggCs9ko0jIzonbO+mzv2lKDlPB5M25oFelnMqv19Q68kaE+mMPb4wTgUF+8F/t1BwfQiI07bbig8YYZL73EM+UN+7GRmSysDW+KWbM3VAbKmtV4RPGMqfbhg List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On 6/24/26 00:36, Ben Mejia wrote: > > > On 5/29/26 5:55 PM, Tomas Vondra wrote: > >> old patches >> ----------- >> >> Those old patches tried to do a fairly small thing during a hash join, >> and that's building a Bloom filter on the inner relation (the one that >> gets hashed), and then use that filter before probing the hash table. >> >> The benefits come from Bloom filters being (fairly) cheap, and a >> negative answer (hash is not in the filter) may allows us to skip a much >> more expensive operation. >> >> The old threads patches focused especially at two hash join cases: >> >> (a) A very selective join, i.e. a significant fraction of outer tuples >> does not have a match in the hash table. >> >> (b) A selective hash join forced to do batching because the hash table >> is too large, and thus forced to spill outer tuples to temporary files. >> >> For (a), the benefit comes from Bloom filters being much cheaper to >> probe than a hash table. The exact cost depends on the implementation, >> sizes, etc. We're in the ballpark of 50 vs. 500 cycles, maybe. But if >> the filter discards 90% of tuples, it can be a big win. >> >> For (b), the filter (for all the batches at once) allows us to discard >> some of the outer tuples without writing them to temporary files. Which >> is way more expensive than probing a hash table. > As it happens, I've been exploring the use of a bitmap filter for the > same two cases you mention. This has some relevance to the issues you > mention in your post about sizing, false-positive rate, etc. > > Instead of a Bloom filter, I chose to use a bitmap filter, with one bit > per bucket on the build side. As the inner table is built, I set a bit > in the bitmap filter for every occupied bucket. If a bucket is empty, > there are no matching hashes and those hash values can be skipped where > appropriate. The advantages of this bitmap over a Bloom filter are: > >  - sizing is pre-determined by nbuckets >  - small bitmaps (4k for 32k buckets) >  - cheaper - nominal cost to set/check bits > > A well-chosen Bloom filter will be more discriminating, but the bitmap > has the same no-false-negatives guarantee and costs much less space and > time to build. > Isn't that pretty much the same thing as a Bloom filter with a single hash function? So it has the same false positive properties, i.e. it may be sacrificing some of the accuracy for not having to calculate any more hashes. Could be a win. > I implemented both of your cases: > > Drop-before-spill: (Case b) > Build per-batch bitmaps during inner partition pass and drop tuples that > don't have a bit set. Saves I/O on tuples that will never match. This > only works for inner and semi joins. > OK, makes sense. > > Single-Batch probe: (Case a) > Only pays off in high-miss-rate joins and a bucket array larger than L2/ > L3 cache. This case has a higher penalty for hash table lookup than the > in-cache bitmap check. This case works in multi-batch, but the I/O cost > dominates and there is no gain. > Right, it's hard to beat the hash table in this case. > > I put runtime guards on both of these; I sampled the drop rate over a > window and disable the filter for the rest of the pass if the rate falls > below a threshold. (~5% for case b; ~25% for case a) > This seems similar to the adaptive behavior I implemented in v2. I haven't thought of the idea of using different thresholds for the two cases - I like that. > The benchmarks are encouraging: > > For case a, I was able to see a best-case improvement of ~15% for > carefully chosen data (dependent on L2/L3 cache size). > > For case b, I tested 3 cases with sparse, average and dense probe hits: > >     sparse probe (~95% miss):           +18% to +36% >     avg probe    (~37% miss):            +9%  to +13% >     dense probe  (FK-like, ~0% miss):    flat, within noise > > (This was on a 8-core x86-64, L1 32KB/core, L2 4MB/core, L3 32MB, 31 GB > RAM. PostgreSQL 19devel, serial hash join, > max_parallel_workers_per_gather = 0, across work_mem = 1-8MB) > > Happy to share the patch and full benchmark data if useful. > I'd be happy to collaborate of some of this, if you're interested. Feel free to post your patch here, or in a separate thread. Up to you. Separate threads might be easier for cfbot to track. I've spent some time hacking on v1, i.e. the pushdown - making the planning work properly, etc. That's mostly orthogonal to this, with some overlap. Ideally we'd want to do both I think. regards -- Tomas Vondra