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 1wgjTc-006MVc-0g for pgsql-hackers@arkaria.postgresql.org; Mon, 06 Jul 2026 13:35:12 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.96) (envelope-from ) id 1wgjTa-000U4L-0B for pgsql-hackers@arkaria.postgresql.org; Mon, 06 Jul 2026 13:35:10 +0000 Received: from makus.postgresql.org ([2001:4800:3e1:1::229]) by malur.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.96) (envelope-from ) id 1wgjTZ-000U41-1s for pgsql-hackers@lists.postgresql.org; Mon, 06 Jul 2026 13:35:09 +0000 Received: from relay1-d.mail.gandi.net ([2001:4b98:dc4:8::221]) by makus.postgresql.org with esmtps (TLS1.3) tls TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (Exim 4.98.2) (envelope-from ) id 1wgjTW-00000001qrg-3Xt7 for pgsql-hackers@postgresql.org; Mon, 06 Jul 2026 13:35:08 +0000 Received: by mail.gandi.net (Postfix) with ESMTPSA id EA8723EDFA; Mon, 6 Jul 2026 13:34:58 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=vondra.me; s=gm1; t=1783344899; 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=DofUzAnNCkq5y5C0re1xroCVAeNXVywwcZW3tVGDh4U=; b=eb7nLUNvrLUwuivNCCi//r/4NUyWRvOM0TR61mjg1yGczd3NFWQBKmBZ5uasDLK2izrGNI nkljc+zmq/m9oiS0wgi7tBzdZo4Ui9Eh1VUwaJPDPL0zfBj2+dZph+e+Yi7WaljZap/UOn uWdgD2VQNFOhZw+gmeVkcBEoEBzGvNHxKXjNlSITdiWGQScSWMHbyWaZizpOzNje93yM+N B7lV0Wu2WYTW82Yz2VHW5JFB0+xEg78skRDFQX/y7Fkw3F/fDJ4wL0awchVEPaR+PvxM/1 p+3Mzm2oJRUaxsCJKk9Qxk5ehvXk5EFO0u97oF6kcC4aTkwrybdy0BzgW7jIQA== Message-ID: Date: Mon, 6 Jul 2026 15:34:57 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Subject: Re: hashjoins vs. Bloom filters (yet again) To: Matheus Alcantara , Andrew Dunstan , PostgreSQL Hackers References: <5cd8c20c-14b5-4b0d-bedc-69bf714e87eb@vondra.me> <12d255f1-533f-414f-8caa-9867759cfce9@dunslane.net> <2f9a99ef-4857-4d4c-8544-30078a38dfa1@vondra.me> Content-Language: en-US From: Tomas Vondra In-Reply-To: Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit X-GND-Sasl: tomas@vondra.me X-GND-State: clean X-GND-Score: -100 X-GND-Cause: dmFkZTEir56IkCruvkn69Q7NGj5VHNVVSPECs4J/Jpc0f33q6i+Np7OamHCgxoArSQTEzfGlX+I3uQFFFnyi8cwpn7iJB61AoKQCzvCce7kMDKyWbpGJOhSsfT8UFa1cstJuPcDijA0HaJ9ULddPO4Eat/xJZurxH6C1o+EqZgKBPYtToajlCccuphKd7EXZw8MBh2RPs18w9atxcEHhQadkFo5Zh48BcEMv2DxMAtldKyuS6Ak2qbX8xIrFqqm7BTq3vAYDjEw0Ml9fELFuxmakTXWj0I/AYO41CkmbL9R5eB6eHmz0tFNogrkiPBAO1XWg0zv2w/gnDitQQHc8mVpEcy6Udg+j98bmPIH9Y6Vajw+A0ChAqMJQ5VC27/H8logTFbTaToIheW95Pvf97DndqJ1aLideR+d0Mf7QXRMwK2+ls1fKnnvra4J8bQlEp1+n++lm57csr181+RFZaY1XbX6hfYbnbnUvE5AwyRIzQpu3UbY2o2Rb2BnB2/5O5jxnuu4vVfe1P3mpButviLlTEz0hhuXuGtidSlrtInskYnqy/LrMQXpIebuo40Xj4S1f5klRGCU1qSMAZN5/zuMX05gBIEAwBsop5O3tm0eib+xH/5h0Tryox0QMnUrWbGZ9rg4F/ijAYrufoC5Nnix2UagOvDoekyC/tzTh0Xuf09EQMg List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk On 7/2/26 22:31, Matheus Alcantara wrote: > On Wed Jul 1, 2026 at 10:25 AM -03, Tomas Vondra wrote: >> (d) At planning / execution this works similarly to v1, except that all >> the decisions were already done. The code in createplan.c and executor >> is more for book-keeping and allowing lookup of filters. >> > > I'm wondering if the adaptive probing can mess the execution of the > choose plan. Let's say that a plan was chosen with bloom filters to > pushdown because it would reduce e.g 50% of the rows, what if at > runtime the bloom filter is proved to not be effective and it get > disabled making the scan node to produce 100% of the rows to the node > above that is not expecting? Do we end up with the same issue "expected > x actual rows"? > > I think that if we keep using the unnefective bloom filter the scan node > will still produce more rows than expected, but perhaps this is easier > to understand? > >> I haven't incorporated the two patches posted by Andrew: >> >> 1) making it work with CustomScans >> >> 2) supporting per-key filters >> >> 3) allow eager creation of filters (disable delayed Hash build) >> >> I agree those seem like a worthwhile improvements, and the patches >> seemed to be OK too, but I was focusing on reworking the planning. Based >> on some off-list discussion, Andrew (or one of his colleagues) should be >> able to adjust those for this v3 patch. >> > > I'm attaching a new v4 patchset incorporating Andrew patches with test > cases. 0001 and 0002 are your v3 untouched, 0003 is some tests added to > exercice the CustomScan path and 0004 is the Andrew changes with a few > changes required from v3 version: > > Unlike the v1 PoC that pushed filters down in create_hashjoin_plan > (where it could simply walk the finished plan tree and accept any scan > node), the filters are now decided during bottom-up path construction, > so a scan only receives a filter if a filter-bearing path was generated > for its base relation. So the main change is teaching path generation > about the custom scan. > I took a quick look at the v4 patches adding the earlier patches from Andrew,, and I think it looks sensible enough for a PoC. Thanks! I think it'd make sense to split some of these into separate commits, as it's not necessarily tied to just CustomScan nodes. And even if it is, it can be a separate easier-to-review change. I propose we move these two "features" into separate commits: - "eager" creation of filters - requesting per-key filters I think it might be useful for other scans (or even non-scan nodes, if we choose to allow filter pushdown for these). And it should be a "generic" thing so that find_bloom_filter_recipient does not need to check if the recipient is CustomScan. In fact, I think this should be part of the information about expected filters "propagated" up during planning. FWIW It might make sense to show these things in the explain, at least for development. AFAICS the CustomScan (used by the tests in 0003) does not actually print the filters, right? And I'm not sure if the per-key filters will be printed at all. But that's minor / easy to fix. To move this whole thing forward, we'll need to figure out what to do about the *big* questions. The four main ones I can think of (based on memory and skimming the XXX comments in the patches) are: 1) selection of 'interesting' filters Currently, we pick 'candidate' filters based solely on the selectivity, but that only works if the scans don't use the filter in some smart way (i.e. when it's evaluated at the very end). But if the custom scan can do something very smart with some filters, the selectivity alone may not be enough. The per-key filters make it even harder, I think. 2) construction of paths with filters (adjusting cost) Right now the paths are constructed by "cloning" existing scan paths, which is what create_filtered_scan_path does. But as already mentioned in some XXX comments, I don't think it can work like this. First, there's the question of costing. Do we need to adjust the cost in some path-specific way? Maybe we don't, at least for built-in core paths? In that case the filters are evaluated on the tuples the path would produce, so maybe we can just add a bit of CPU cost per tuple, to account for the evaluation of the filter (and reduce the row estimate). Ff the filter is selective it'd still win, as expected. But it's a bit dubious even for built-in core paths, because we'd operate only on paths that already went through add_path and survived the selection. Maybe we already eliminated some interesting path? With "smart" CustomScan it's a bigger problem, though. If the scan is doing something smart with the filters, it likely needs to adjust the cost in a much more complex way. But the cloning does not allow that. But more importantly, we can't copy CustomPath like this, because the comment in pathnodes.h says: * Core code must avoid assuming that the CustomPath is only as * large as the structure declared here; providers are allowed to * make it the first element in a larger structure. (Since the * planner never copies Paths, ... And we'd violate that. (Maybe this is a problem for the in-core scan paths too?) Some particular CustomPaths might be OK, but we can't know which ones are. Maybe we could require scans wih CUSTOMPATH_SUPPORT_BLOOM_FILTERS to allow such cloning, but it seems pretty fragile. 3) pushdown to multiple consumers (scans of partitioned tables) Right now, we only allow pushdown to a single consumer. But with partitioned tables that won't work - each partition scan is a consumer of the same join (unless it's a partition-wise join, probably). We need to make that work. It probably can't happen right now, because we don't push through Append nodes (per find_bloom_filter_recipient), but that seems like something we should allow. Does not seem extremely particularly hard to address, though. The find_bloom_filter_recipient simply needs to collect a list of recipient nodes, not just the one. 4) parallel hash joins There's some complexity due to having to put the filter into shmem, but the v2 patch solved that I think, and we can mostly just copy that. I don't think we need to solve that now. I think (3) and (4) are somewhat mechanical. I know, it can easily be much harder I envision. We won't know until we try. For (1) and (2), I think we need to make this work more like how we build paths for pathkeys. Do some sort of initial selection of interesting filters, without trying to be very smart. So we'd maybe check the filter discards > 30% of tuples, but without trying to keep only the "top three" filters. Then we'd generate the paths for all the possible combinations of filters, not by cloning but by passing the expected filters to the create_ function, and constructing a new path. If the scan can do something smart - cool, it'd calculate the cost. In the worst case it'd apply the filter at the end, with the generic cost / rows adjustment. The paths with matching expected filters would still compete (just like for matching pathkeys), of course. There's still need to be some limits, though. If the table has 10 joins, we can't generate paths for each possible combination of filters, that'd be 2^10 combinations. But there are some inherent limits too, I think. For example, once we discard ~90% tuples it probably does not make much sense to keep applying additional filters. If each filter discards ~50% tuples, that's 3 filters. So even with 10 joins we'd be limited to ~120 paths (which is still a lot, but way lower than 2^10). We could still have some "heuristics" limits too, e.g. don't keep all these paths, but prune them very aggressively and pick only the best ones - just like we do now by only picking 3 most selective filters, except that we'd do that based on proper costing (and not selectivity). I plan to experiment with (1) and (2) soon, but it'd be great if you could experiment with that too, and/or think about alternative ideas. regards -- Tomas Vondra