Skip to content

Bitfilters

Probabilistic set-membership filters as DuckDB columns.

1,635,646
extension loads · last 90 days
On this page

Technical Overview

A set-membership answer that fits in a BLOB

What this extension is for

  • Skip a join when the answer is definitely no: Build a filter from the small "interesting" side of a join, then test the large side against it. False positives let an unwanted row through; false negatives never happen — so the cheap probe + expensive verify pattern is exact for correctness.
  • Per-partition data skipping: Materialize one filter per logical partition, store alongside metadata, and probe at query time to skip whole partitions that can't possibly contain the key. For high-cardinality lookups against fact tables this routinely skips 99%+ of partitions.
  • Compact set artifacts: A Binary Fuse 8 filter is under 9 bits per key — three orders of magnitude smaller than the source set for typical UUID workloads. Persist it in a Parquet column, ship it to another process, embed it in a manifest.
  • DuckDB-native join filters: One family emits filters in the same internal format DuckDB uses for its own runtime join filters — persist a filter, then re-use it in custom data-skipping logic the optimizer recognizes.

Filter families

  • Quotient — dynamic sets: The only family here that supports deletion and resize. Pick it when the underlying set evolves at runtime. Tunable slot count and FPR. See Quotient filter.
  • XOR — static, cross-system compatible: ~9 bits per key at ~0.39% FPR (8-bit) or ~18 bits per key at ~0.0015% FPR (16-bit) — about 20% smaller than a Bloom at the same FPR. Built once, queried many times. See the Xor filters paper (Graf & Lemire, 2019).
  • Binary Fuse — modern best-in-class: Typically lands under 9 bits per key with very fast construction and lookups — currently the most space-efficient static filter family. Default choice for read-heavy, append-only sets.
  • DuckDB Bloom — native compatibility: Speaks DuckDB's internal Bloom filter format and hash layout, so a persisted filter slots into the engine's own data-skipping path.

How it works

  • Hashes go in, BLOBs come out: Hash your input first — DuckDB's built-in hash() is fine for most cases. For specific algorithms (xxh3_64, MurmurHash3, rapidhash), pair with the hashfuncs extension. Filters distribute keys based on hash bits — quality of the hash matters.
  • Filters as columns: Every filter is just a BLOB. Store it in a regular column, persist to Parquet, send over the wire, hold in a CTE, partition by anything — there is no special filter type, only the matching *_contains probe function.
  • No false negatives, ever: The filter never lies about absence: a false from a probe means the key is definitively not in the source set. False positives are bounded — measure them by probing known non-members and confirm the rate matches the family.
  • Set-membership only — not cardinality: Bitfilters answers "is X in the set?". For cardinality estimation ("how many distinct items?") and set intersection sizes, use the sibling datasketches extension — it has Theta sketches and HyperLogLog for that workload. The two extensions cover complementary problems.

Honest limitations

  • False positives are inherent: Every family here trades exactness for memory. A true from *_contains means probably present, not definitely present. If your downstream cannot tolerate even one wrong yes, pair the probe with an exact verify — the filter just narrows the candidate set.
  • Filters cannot enumerate: There's no way to list the members from a filter — it's set-membership only. If you need the elements themselves, store them; if you need cardinality, use datasketches.
  • Static families don't support updates: XOR, Binary Fuse, and DuckDB-Bloom filters are built once and immutable. Adding a single key requires a full rebuild. Use quotient_filter when the set genuinely churns, or rebuild on a cadence that matches your write throughput.
  • Tiny sets don't earn the filter: For a few thousand elements an in-memory hash set or a sorted array is faster and exact. Bitfilters earns its keep when the source set is large enough that storing the full set is what hurts — millions of keys, per-partition replication, network ship costs.

Common Use Cases

Deep Dive

Technical Details

Install

INSTALL bitfilters FROM community;
LOAD bitfilters;

Quick Start

Build an xor8 filter from hashed keys

-- 1% false-positive rate, ~9 bits per key
WITH f AS (
  SELECT xor8_filter(hash(email)) AS filter FROM users
)
SELECT email,
       xor8_filter_contains(f.filter, hash(email)) AS might_be_known
FROM new_signups, f
LIMIT 10;

Per-partition Quotient filter — supports deletion and resize

SELECT bucket,
       quotient_filter(16, 4, hash(id)) AS filter
FROM ids
GROUP BY bucket;

Reference

Extension Contents

Quick reference to all available functions and settings organized by category.

Name Description
Binary Fuse Filters
binary_fuse16_filter() Aggregate that builds a 16-bit Binary Fuse filter — same family as binary_fuse8 but with much lower false-positive rate at the cost of ~2× the memory.
binary_fuse16_filter_contains() Test whether a hash value is a member of a 16-bit Binary Fuse filter.
binary_fuse8_filter() Aggregate that builds an 8-bit Binary Fuse filter from hashed keys.
binary_fuse8_filter_contains() Test whether a hash value is a member of an 8-bit Binary Fuse filter.
DuckDB Bloom Filters
bitfilters_duckdb_bloom_filter_create() Build a Bloom filter compatible with DuckDB's internal join filter format.
bitfilters_duckdb_bloom_filter_probe() Test whether a hash value might be a member of a DuckDB-compatible Bloom filter built by bitfilters_duckdb_bloom_filter_create.
bitfilters_duckdb_hash() Hash function compatible with DuckDB's internal Bloom filter probe layout.
Quotient Filter
quotient_filter() Aggregate that builds a Quotient filter from hashed keys.
quotient_filter_contains() Test whether a hash value is a member of a Quotient filter.
XOR Filters
xor16_filter() Aggregate that builds a 16-bit XOR filter from hashed keys.
xor16_filter_contains() Test whether a hash value is a member of a 16-bit XOR filter.
xor8_filter() Aggregate that builds an 8-bit XOR filter from hashed keys.
xor8_filter_contains() Test whether a hash value is a member of an 8-bit XOR filter.

API Reference

Function Documentation

Practical Examples

Cookbook

Real-world recipes and patterns for common use cases.

Platform Support

Compatibility

Extension availability may vary by platform and DuckDB version. Check below to ensure this extension supports your environment before installation.

Quick Facts

Release status Stable
Software License MIT
Pricing Free
Written In C++
Source Available Yes
View on GitHub
Usage
1,635,646
loads · last 90 days

Platforms

  • Linux x86_64 aarch64
  • Linux (musl) Not available
  • macOS Intel Apple Silicon
  • Windows x86_64
  • WASM eh mvp threads
Compiled binary sizes
Platform Architecture Size
Linux x86_64 3.28 MB
Linux aarch64 2.91 MB
macOS Intel 2.11 MB
macOS Apple Silicon 1.80 MB
Windows x86_64 7.43 MB
WASM eh 59.8 KB
WASM mvp 46.7 KB
WASM threads 59.5 KB

Compressed download size from the Haybarn extension repository.

DuckDB & Haybarn

Release calendar