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 1whEFi-0005hZ-1A for pgsql-hackers@arkaria.postgresql.org; Tue, 07 Jul 2026 22:26:54 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1whEFg-0031yB-31 for pgsql-hackers@arkaria.postgresql.org; Tue, 07 Jul 2026 22:26:53 +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 1whEFg-0031xM-1d for pgsql-hackers@lists.postgresql.org; Tue, 07 Jul 2026 22:26:53 +0000 Received: from relay6-d.mail.gandi.net ([2001:4b98:dc4:8::226]) by magus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1whEFe-000000007CS-1w7W for pgsql-hackers@postgresql.org; Tue, 07 Jul 2026 22:26:52 +0000 Received: by mail.gandi.net (Postfix) with ESMTPSA id 479D73E97F; Tue, 7 Jul 2026 22:26:43 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vondra.me; s=gm1; t=1783463204; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=16ZM/enATJANkSDS1LHmnK6sOUdkJ1TcHFR8QXITsRA=; b=BnhW9OS/j5nzRhthH9RZvR2vkQqQAq0VrgF1m/2tBJTrnk38sS0yhElhpcjPXB5U5lduec 94morrQQXiEDgLTFzcoNNItEWAuu/YvRpEz07VtQWnQJF0A2+9bI04Q8ctmFLa3BhIGRxu afvT74cpnsK8wZz/dWQkI11UbLFhRSGbaW1WfiJuN3qKQ97+qcG5TE8dDpbIOd/vDUMFeT kM8YZo3hDsoP02de20gMF31FJJbkXUpgJ608dmUBv03cZK4bcIufJu2JiDmXSfZ9aDJ1ms rbxr/SbG/0LgUccRUL4aYWw4TqQfYoVuN1KgrBLctzuCrj2J9g0VrT/qLe+N4Q== Message-ID: <16c60936-9b6e-4649-a34a-a155f3d0bd07@vondra.me> Date: Wed, 8 Jul 2026 00:26:41 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: hashjoins vs. Bloom filters (yet again) To: Robert Haas Cc: Oleg Bartunov , PostgreSQL Hackers References: <5cd8c20c-14b5-4b0d-bedc-69bf714e87eb@vondra.me> <838eb84c-7da6-46ec-bc27-df9ca2009c67@vondra.me> Content-Language: en-US From: Tomas Vondra In-Reply-To: Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-GND-Sasl: tomas@vondra.me X-GND-Cause: dmFkZTFmwdPYr4LULywFYmvLBw0vOHoE8BCJNUCsY+xFXpHjdybtHJfkq6+ebnrUqGSocem/12pFi2Q5IR7FDreaKm7jJS0yuDWpLpENa9KsdYB+m37VMvB4fl6y98Kpz44FK1ur1xWUQNwY6kUcsEL5xDkmEoUJP9svFTrgcT+AShKGMBrWNy0z7Q7qG6vifz8NxCDUUepYU0JL1/hgNx/yZkEhVMNMXJ1FAXG6XHYVHAFYX1pcavshQVdxm0O9y+/OwulfsHqGWvuGttO+mpYaDN7aRYLlXCTNYsIyuckGYcV6ArpD6ZpHAlaqzv4RVB18lElUHNcV52wEDYLw2Cb9knnNNISLU6sd6xfuxEUhQjMbGr8V+ksAo/pYGu8rYEfs6bDUpzQIVWFFUARVWYNm9wRS+uSE4ODjGDXXEGj+3WiQFvVfQBJ/vWscZfeM9rpU9b8FHIgViufcVQaLbb07gbd/UkwJ1tzhUNVFfbgvx/3Qy4di5IEs32TtNSSnT/9cherQ2ShG0iIa870LEvK241I7llUMc/Z/BJguK+sDxTD5caLTZuet8AC/yDBZxROk4ursE+O+/mrySqV4K79BDe4pqwjg4q0x30ic4illj+B0ZEjUrb624XMRYnY8nDRMEz22iAQ7h1GJwYVCTLW6FDYo8R5wYi2RsgYc+IDVVOFjow X-GND-State: clean X-GND-Score: -100 List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On 7/7/26 23:19, Robert Haas wrote: > On Tue, Jul 7, 2026 at 2:39 PM Tomas Vondra wrote: >> I don't disagree, but it's that what all the push-down patches in this >> thread are already doing? When I said "tied to hash join" I meant the >> filter would be built by a hash join, not that all the joins have to be >> hash joins. It can certainly by pushed through NL/merge joins. > > I haven't actually looked at the patches -- I was just responding to > the email. Sounds like I misunderstood. > >> You're definitely right the extra paths may be rather expensive, and >> we'll need to keep that under control. If a query has 10 joins, we can't >> realistically consider all 2^10 of filter combinations. > > Yeah. > >> But I think there are more options how to limit the cost. For example, >> it's unlikely to help when the scan already returns very few tuples. So >> maybe we could consider filters only when the row count is above 1000, >> or something like that. And maybe we could do something similar based on >> cost? But that would ignore savings above the scan (e.g. maybe one of >> the later joins is very expensive). > > I'm not sure which scan you mean when you say "when the scan already > returns very few tuples." Suppose we propose to join A to B. We plan > to use a hash join and use B as the build table and A as the probe > table. So, if I understand correctly, we'd build the bloom filter on B > and use it when scanning A. Do you mean that the filter is unlikely to > help when B is small, or when A is small? When A is small, i.e. the outer side of the join (not the side on which we built the hash / filter). The idea is that if the outer side has 1000 rows, even if we eliminate 90% of them it may not pay for itself (because of the cost of extra paths during planning, building the filters etc.). One of my ideas for keeping the number of paths under control was to stop "adding" filters to a path once the cardinality drops below some threshold. Let's say we start with a scan returning 1M rows, and we add 2 filters eliminating 90% tuples each. Cool, now it's expected to return 10k rows. Should we consider a version with a 3rd filter? Maybe it'll help a little bit, but the benefit is certainly much smaller than for the first two filters. > Neither is obvious to me: if > B is small, the hash table will be small, which might make it cheap to > probe, but it also makes the Bloom filter small, so maybe that's > *really* cheap to probe, plus a small Bloom filter is enough to have a > really low false-positive rate. The size of B (on which the hash table is built) is a very poor signal. What matters is the selectivity of the join, and a tiny table can eliminate 1% or 99% - it does not depend on the size. Yes, a tiny table may be cheap to probe (the Bloom filter will still be cheaper, but it'll be closer). For large hash tables the trade offs is a bit different, especially when the join start spilling. But even then it's more about what fraction of tuples it eliminates. But focusing on the relative cost of probing the hash table vs. probing the Bloom filter is missing the point - it's about *when* you probe it. With the pushdown the probe happens much earlier, before doing a lot of other relatively expensive stuff. As an extreme case, imagine we push the filter into a ForeignScan. The foreign scan could copy the filter to the remote side, and discard all the tuples there, saving all the network costs. (Yes, the patches don't do this - yet, but it's not a huge change.) Yes, maybe we could skip building the filter and just probe the hash table directly. That only works in some cases (e.g. the FDW case would not work), and only when the hash table is "complete" (no spilling). > If A is small, you're not going to > save as much in absolute terms, but you save the same thing > percentage-wise as on a larger table. > > It seems like the most important thing -- which I'm aware you already > mentioned -- is how likely it is that a row in A joins to a row in B. > If the answer is "not very likely," the filter has a good chance of > working out. > Exactly. And then it's also about the cost of what happens in between the probe and the join. >> I think a lot of the regressions can be handled by adaptive behavior at >> execution time. Like, we can postpone building the filter until after >> we've already probed the hash table to know it's worth it. Or disable >> the filter when it becomes ineffective. The v3/v4 patches already do >> some of this, IIRC. > > Yeah, adaptive behavior is great. We need to be careful not to add too > much complexity, on the one hand, or to expect perfection, on the > other. But it's a good way of mitigating downsides. > 100% agreed >> Of course, even with the adaptive behavior we're "stuck" with the new >> plan (which may differ from the plan we'd have picked otherwise). But >> I'm not sure it's really that different. Because really, the impact is >> somewhat limited. > > This doesn't especially worry me: any plan can be wrong. If the > planner goes wrong substantially more frequently with this patch than > without, of course that would be bad. But if it just goes wrong for a > different set of queries, I think that's just life. > OK >> What happens is that we pick the K most selective joins, and build >> filters for those. And then we plan with that. Those selective joins >> would be at the very bottom of the original plan, because we prefer to >> do selective joins first. > > This is a clever idea. > >> Let's say a plan has 4 joins. >> >> X >> / \ >> B2 X >> / \ >> B1 X >> / \ >> A2 X >> / \ >> A1 T >> >> Most likely, A1 and A2 joins are the two most selective, so we get to >> build filters for those, and push them to scan on T. Which means the >> filtering is applied in the scan, and the joins themselves won't reduce >> the cardinality at all! >> >> The B1 and B2 joins become "more selective", and in the new plan >> (selected with filter pushdown) will be performed first: >> >> X >> / \ >> A2 X >> / \ >> A1 X >> / \ >> B2 X >> / \ >> B1 T (filters from A1/A2) >> >> But the A1/A2 filtering still happens *before* B1/B2, thanks to the >> filter. So B1/B2 are still planned with the same selectivity and row >> counts as before, and so will most likely use the same join algorithm. > > They could switch to a nested loop, though, which is a little tricky > to reason about: any individual join could be better as a nested loop, > while the set of joins taken together might be better if you use all > hash joins so that the filters can be combined. > No, I don't think they could switch to nested loops. That's the whole point - the B1/B2 joins are planned with *exactly* the same row estimates as before, because A1/A2 still eliminate the tuples before B1/B2. So if they are nested loops in the new plan, it'd be the same in the old plan too. Of course, it can happen that B1/B2 really are nested loops, and that it's a poor choice because A1/A2 eliminated much smaller fraction of tuples than we thought. But that affects both plans the same way. >> So the impact on the query plan is actually much smaller than it might >> seem. Which likely limits the risks quite a bit. >> >> Of course, this is just a very simple example. Maybe it's riskier for >> more complex joins, not sure. > > I don't see that a more complex tree magnifies the risk; I think it's > just about how sure you can be that you're actually going to filter > something out. Your chances of winning probably actually grow with the > number of joins for which you build the filter, because I guess you > can bitwise-AND all of those filters and just test the result. If even > one of the filters is highly selective, that may gain you enough to > pay for the cost of building all of them. This is not guaranteed, of > course, but the risks and rewards seem asymmetric: a useful filter > seems as though it can win much harder than a useless filter can ever > lose. I could be wrong, but my guess is that we only need to do a > moderately good job avoiding the worst case. > I admit I'm hedging a little bit - it's probably fine even for complex plans, but I haven't done enough tests to make such a clear claim. Maybe you're right more filters make it safer. But more filters mean we need to consider more paths, so it's a trade off. Unless we figure out a good way to pick "interesting" combinations. I'm not sure about combining the filters by bitwise-AND. Maybe it could work and help a little bit, but it'd also require all the filters to have the same size. It's worth testing, but I think the cost of the filter probe does not matter all that much (compared to the savings by eliminating the tuples much earlier). regards -- Tomas Vondra