Incremental Update: v2 Patch Series (Hash Join-Local Bloom Filters) and Architectural Discussion
Major Development: v2 Patch Series — Bloom Filters Within Hash Join Scope
Tomas Vondra posted a new, independent patch series (labeled v2 but orthogonal to the v1 pushdown patch) that adds Bloom filters within the hash join node itself — both for serial and parallel joins. This is a deliberate scoping-down to tackle shared prerequisite problems (adaptive behavior, sizing, batching) before attempting full pushdown.
Key Technical Details of v2
Adaptive Build Logic:
- For
nbatch=1(in-memory): delay filter construction until 1000 lookups have been observed; only build if <90% of lookups find a match - For
nbatch>1(batched/spilling): must build the filter immediately before spilling, because tuples from later batches need to be filtered against the complete build set - Periodic rechecks allow late filter construction if match rates drop
Adaptive Probe Logic:
- Track probes vs. rejections continuously
- If <10% of probes are rejected → disable the filter (it's not paying for itself)
- When disabled, sample 1% of probes to detect if conditions change
- Re-enable if >20% rejection rate detected in samples
Parallel Hash Join Extension (0002):
- Direct extension of serial case but operates on shared-memory hash tables
- Inherently more complex due to parallel hash build coordination
Sizing/False-Positive Trade-off Issue Identified:
- Current
lib/bloomfilter.ctargets 1-2% FPR, sometimes achieving 0.077% FPR - Vondra questions whether accepting ~10% FPR for a much smaller filter and fewer hash functions (fewer bits to check per probe) would be net-positive
- Example: with work_mem=64MB, filter is 16384kB with 10 hash functions for a join using 82784kB across 16 batches
Performance Results (v2, scale=100: 100M fact × 10M dimension)
Serial (workers=0):
- 20% faster for joins with >50% outer tuple discard rate
- ~2x faster at 5% selectivity (95% discarded)
- Adaptive thresholds correctly identify the break-even point
Parallel:
- Benefits more limited and inverted pattern: serial benefits most from batched joins, parallel benefits most from unbatched joins
- Vondra suspects possible bug in parallel path or hardware bottleneck; requests review
Planning/Costing: Academic Paper Reference
Vondra references a 2025 paper "Including Bloom Filters in Bottom-up Optimization" (arXiv:2505.02994v1) that directly addresses the planning problem from the prior analysis. Key observations:
- Paper's solution resembles the "tracking separate path sets" idea but uses a two-phase process
- Vondra considers this too invasive (would require fundamental rework of join rel construction)
- Instead proposes a pathkeys-like analogy: define "potentially interesting filters" at baserel scan time, construct extra paths only for those, and reject at join planning if the join isn't a hash join
Architectural Framing: Runtime Knowledge Pushdown
Oleg Bartunov articulated a broader architectural vision distinguishing two roles:
- Local HJ optimization — no physical ordering requirements, pure CPU savings from avoiding probes/spills
- Scan-layer pushdown — requires coarse-grained structures to skip (partitions, row groups, dictionaries, BRIN summaries, min/max metadata)
He frames the real feature as a pluggable runtime-filter mechanism — not Bloom-specific — where join-derived knowledge flows downward against normal tuple flow, consumable by heap, CustomScan, FDW, columnar AMs, or chunk-based storage at whatever granularity they support.
Top-Down Planning Alternative
Andrei Lepikhov suggests using create_upper_paths_hook for a "top-down" iteration after bottom-up planning (referencing his "MiddleOut" approach). This could theoretically resolve the planning incompatibility but Vondra considers it too complex to integrate with the already-complex Bloom filter work.
Interaction with Executor Batching Work
Vondra notes that ongoing executor batching work could provide "row group" semantics even for in-HJ-node filters, partially bridging the gap between local and pushdown approaches (though tuples still flow up to the join node).