Remove structurally redundant BF pairs - #27
Open
jianhongshi123 wants to merge 6 commits into
Open
jianhongshi123 wants to merge 6 commits into
jianhongshi123 wants to merge 6 commits into
Conversation
jianhongshi123
force-pushed
the
dev/redundant_pair_removal
branch
from
September 2, 2026 14:59
4708f10 to
86bc6f1
Compare
jianhongshi123
marked this pull request as ready for review
September 2, 2026 16:54
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
I observed on TPC-H Q10 some Bloom filter pairs are built but provide no filtering benefit, so constructing them will add pure overhead. More generally, predicate transfer can generate redundant Bloom filter pairs, some of which can be proved redundant before execution stage.
Exact Redundancy Condition
For an integer build-side join column, a Bloom filter pair is redundant if the exact NDV equals the size of the build-side value range and the probe-side value range is fully contained in the build-side range.
In this case, every integer value in the build-side range must be present, so the Bloom filter cannot eliminate any probe-side tuples.
Challenges and Alternatives
The exact condition is difficult to apply directly for two reasons. First, predicate transfers are not independent. The min and max given by DuckDB is from base table, so the detector needs to determine whether build-side table is already filtered before applying the exact redundancy condition. Therefore, both local predicates and incoming probe filters need to be considered when applying it.
Second, DuckDB does not provide exact NDV. I initially used DuckDB's approximate NDV, but that was very inaccurate which caused a high false-negative rate and removed few proven redundant pairs.
I considered the following alternatives:
* DuckDB does not give exact NDV, so this option is not available in practice.
Approximate NDV is directly available in DuckDB, but in our TPC-H experiments it produced a high false-negative rate. Adding a threshold improves coverage but requires parameter tuning. Declared PK/FK constraints avoid statistical estimation, but they are not always available and cover only a subset of the redundant pairs captured by the Exact Redundancy Condition.
Current Design
The current design reuses statistics already maintained by DuckDB to detect redundant Bloom filter pairs before execution, without additional data scans, runtime profiling, or empirically tuned parameters.
Redundancy Detection
This PR first check whether the probe-side value range is fully contained in the build-side range. It then use two complementary checks that target different common join key structures:
1, 2, 3, ..., N.A Bloom filter Pair is considered redundant when the range check pass and either of the two structure checks pass.
Filter State Tracking
Redundancy Detection uses statistics from base table, so we need to also prove that the actual domain have not changed and still represented by the base statistics when reaching the Bloom filter. This PR processes Bloom filter pairs in transfer order and tracks the incoming filter state of each table.
Each table has one of these three states:
For every pair, if there is local predicate on build table which can not prove to be non-selective on either table, then the base statistics can be changed when reaching the Bloom filter. In this case, the filter will be directly marked not removable.
The following reasoning is built on the assumption that no filtering local predicate on build table:
The Bloom filter pairs are visited in transfer order, so table is visited as build table after all of its predecessors are visited in this pass. There can be disconnected components in our DAG, but it does not matter due to this. All tables start in the NONE state. When a pair cannot be removed, its probe-side table records the build table as an incoming filter source. A second incoming filter changes the state from ONE to MULTIPLE. If a pair is removed, it does not change the probe-side state.
One special case is the root. If the state of the root table is ONE when visited, and the beginning of the backward pass passes the redundancy check, this pair is redundant because the root has only been filtered by the same table to which the filter is now transferred back. Therefore, transferring the filter back cannot eliminate any additional rows. If the root is marked redundant due to this logic, the same reasoning can propagate along the backward pass.
A pair is eligible for redundancy detection when its build table state is NONE, or when it satisfies the turnaround case above. All pairs identified as redundant are removed together after the iteration.
Evaluation
To isolate the effect of redundant pair removal, I compared this optimization enabled and disabled both under TPC-H SF=100 Threads=8
There are 10 queries with Bloom filter and inserted and 5 of them are affected by this optimization. All 5 queries get performance gain in the current ablation setting, with geometric mean speedup 1.113x across affected queries. Most notably, Q10 was the biggest regression without this optimization and was 0.806x baseline DuckDB. With this optimization, the performance improves to 1.072x baseline DuckDB.
Across the complete TPC-H workload, Robust runtime decreases from 38.196s to 36.954s, a 1.034× speedup from redundant pair removal. The DuckDB baseline remains similar between the two runs.
Limitations
The current redundancy checks are approximate and may produce false predictions. However, a false-positive removal only affects performance, not correctness, since only inserted Bloom filter are removed and the original query is still executed.