Quotient Filter
A quotient filter is a cache-friendly, compact approximate-membership hash table introduced by Michael A. Bender and colleagues in 2012 that splits p-bit fingerprints into a q-bit bucket quotient and an r-bit stored remainder using three metadata bits per slot.
Quotient Filter is a space-efficient, cache-local probabilistic data structure in computation and algorithms that answers approximate set-membership queries over a dynamic set S with zero false negatives and a tunable false-positive probability ฮต โ 2โr. Introduced by Michael A. Bender, Martin Farach-Colton, Rob Johnson, Russell Kraner, Bradley C. Kuszmaul, Dzejla Medjedovic, Pablo Montes, Pradeep Shetty, Richard P. Spillane, and Erez Zadok in 2012โbuilding on Donald Knuth's 1973 exercise on quotienting in The Art of Computer Programming and Cleary's 1984 compact hash tablesโa quotient filter stores p-bit fingerprints inside an open-addressed linear-probing hash table of m = 2q slots by implicitly encoding the high-order q bits in the slot address and storing only the low-order r = p โ q remainder bits alongside 3 structural metadata bits per slot.
In computational complexity theory and external-memory database indexing, a classical Bloom filter issues k independent random bit probes across the entire array for every lookup or insertion, incurring up to k CPU cache misses or flash-storage page reads, and neither supports deletions nor allows resizing without re-hashing the original keys. Because a quotient filter stores full p-bit fingerprints in sorted canonical order across contiguous linear-probing clusters, every lookup, insertion, and deletion accesses at most one or two contiguous CPU cache lines, supports dynamic deletions without false negatives, and enables linear-time merging and doubling/halving of filter capacity without re-accessing the original keys.
Quotienting Technique, Metadata Bits, and Run/Cluster Geometry
Fingerprint Quotienting into q Quotient Bits and r Remainder Bits
Let S โ U be a dynamic set of n elements, and let h : U โ {0, 1}p be a uniform hash function that maps each key x โ U to a p-bit fingerprint f = h(x). In a table of m = 2q buckets, quotienting partitions the p-bit fingerprint f into two bit fields:
- The Quotient fq (High-Order q Bits): fq = โf / 2rโ โ {0, 1, ..., 2q โ 1}, which designates the element's canonical slot (home bucket) in the array T[0 .. 2q โ 1].
- The Remainder fr (Low-Order r Bits): fr = f mod 2r โ {0, 1, ..., 2r โ 1}, where r = p โ q. Only the r-bit remainder fr is stored inside the slot array.
If a remainder fr always resided at its canonical slot fq, the complete p-bit fingerprint could be reconstructed losslessly via the identity f = (fq โช r) | fr. When multiple keys share the same quotient fq (a quotient collision) or are displaced by earlier collisions under linear probing, a remainder fr is shifted to a later slot s โฅ fq (modulo 2q).
Three Metadata Bits: is-occupied, is-continuation, and is-shifted
To reconstruct the original canonical quotient fq of every stored remainder despite linear-probing shifts, a quotient filter maintains two ordering invariants: all remainders sharing the same quotient fq are stored contiguously in ascending remainder order in a run, and the runs themselves appear in ascending order of their canonical quotients fq. Each slot s โ {0, ..., 2q โ 1} carries 3 metadata bits alongside its r-bit remainder slot:
- *is-occupied*[s]: Set to 1 if and only if there exists at least one stored fingerprint in the filter whose canonical quotient equals s (fq = s), regardless of whether that run is currently stored at slot s or has been shifted rightward to a later slot.
- *is-continuation*[s]: Set to 1 if and only if slot s holds a remainder that belongs to the same run (same canonical quotient fq) as the remainder in the directly preceding slot s โ 1. Consequently, *is-continuation*[s] = 0 marks the start of a new run (if slot s is non-empty).
- *is-shifted*[s]: Set to 1 if and only if the remainder stored in slot s does not reside at its canonical slot (s โ fq).
Together, these three bits define the structural hierarchy of runs and clusters:
- A run is a contiguous block of slots storing all remainders that share a single canonical quotient fq; its first slot has is-continuation = 0 and all subsequent slots in the run have is-continuation = 1.
- A cluster is a maximal contiguous sequence of occupied slots beginning at an anchor slot s0 that holds an unshifted run-start (*is-occupied*[s0] = 1, *is-continuation*[s0] = 0, *is-shifted*[s0] = 0) and continuing through all directly following slots that have is-shifted = 1.
| Metadata Tuple (is-occupied, is-continuation, is-shifted) | Structural Interpretation of Slot s | Quotient Recovery Role |
|---|---|---|
| (0, 0, 0) | Empty Slot: No run starts here, and canonical bucket s has no elements | Terminates any preceding cluster |
| (1, 0, 0) | Cluster Anchor (Unshifted Run Start): First element of the run for quotient s, sitting at its home slot s | Establishes synchronized base quotient qcurr = s |
| (0, 1, 1) or (1, 1, 1) | Run Continuation: Subsequent element of the currently active run (shifted from earlier slot) | Retains current run quotient qcurr |
| (0, 0, 1) or (1, 0, 1) | Shifted Run Start: First element of a new run that was pushed rightward from an earlier canonical slot fq < s | Advances qcurr to the next slot โฅ qcurr + 1 where is-occupied = 1 |
| (1, 0, 0) โ (0, 0, 0) | Cleared Run: Only valid during transient update before is-occupied is cleared | โ |
Lookup, Insertion, Deletion, and Cache-Local Merging
Cluster Scanning and Membership Query Algorithm
To test whether a key x with fingerprint f = (fq, fr) is present in a quotient filter:
- Fast Rejection: Inspect *is-occupied*[fq]. If *is-occupied*[fq] == 0, no element with quotient fq is stored in the filter; return false in a single memory read.
- Walk Backward to Cluster Anchor: Starting at s โ fq, walk leftward (s โ s โ 1) while *is-shifted*[s] == 1. During this backward scan, count the number c of slots encountered that have *is-occupied*[s] == 1. The scan stops at the cluster anchor s0 where *is-shifted*[s0] == 0.
- Walk Forward to Target Run: Because runs occur in the same order as the is-occupied = 1 bits inside the cluster, advance s rightward from s0 until c run starts (*is-continuation*[s] == 0) have been passed. Slot s now points to the start of the run for quotient fq.
- Scan Target Run: Compare fr against the remainders in the run (while is-continuation == 1 in subsequent slots). Because remainders within a run are sorted in ascending order, stop as soon as a remainder โฅ fr is seen or the run ends.
When the load factor ฮฑ = n / 2q is bounded away from 1 (e.g., ฮฑ โค 0.75), standard linear-probing analysis by Knuth shows that the expected cluster length is O(1 / (1 โ ฮฑ)2) = O(1) slots (typically 2 to 6 slots), so the entire backward and forward walk executes inside a single 64-byte CPU cache line.
Dynamic Deletions and Keyless Resizing / Merging
Unlike a Bloom filter, deleting a fingerprint (fq, fr) from a quotient filter simply removes fr from its run, shifts subsequent remainders in the same cluster leftward by one slot (clearing is-shifted on any run that returns to its canonical home slot), and clears *is-occupied*[fq] โ 0 if the deleted remainder was the sole member of its run.
Furthermore, because every p-bit fingerprint f = (fq โช r) | fr can be reconstructed sequentially by streaming through the table in O(2q) time, two quotient filters QF1 and QF2 can be merged in a single sequential pass (identical to merging two sorted arrays in merge sort), or doubled in capacity (qโฒ = q + 1, rโฒ = r โ 1) by moving the most significant bit of each remainder fr into the least significant bit of its quotient fq without storing or re-hashing the original keys.
| Filter Architecture | Bits per Slot at ฮต = 2โr | Deletions Supported? | Resizable / Mergeable Without Keys? | Memory Locality per Lookup | High-Load Behavior (ฮฑ โ 95%) |
|---|---|---|---|---|---|
| Standard Bloom Filter | 1.44 r bits/element | No | No (fixed m only) | k = 0.693 m/n random misses | Degrades False-Positive Rate ฮต |
| Standard Quotient Filter (2012) | (r + 3) / ฮฑ bits/element | Yes (O(1) amortized) | Yes (O(m) sequential stream) | 1 cache line (contiguous) | Linear probing cluster growth at ฮฑ > 80% |
| Rank-and-Select QF (RSQF, 2017) | (r + 2.125) / 0.95 bits/element | Yes (O(1)) | Yes (O(m) sequential stream) | 1 cache line (SIMD POPCNT + PDEP) | Stable up to ฮฑ = 95% load |
| Cuckoo Filter (Fan et al., 2014) | (r + 3) / 0.95 bits/element | Yes (O(1)) | No (partial-key XOR fixed to 2q) | 2 cache lines | Stable up to ฮฑ = 95% via cuckoo hashing |
Rank-and-Select Quotient Filter (RSQF) and Counting Quotient Filter (CQF)
In 2017, Prashant Pandey, Michael A. Bender, Rob Johnson, and Rob Patro introduced the Rank-and-Select Quotient Filter (RSQF) and Counting Quotient Filter (CQF). Instead of walking backward across long clusters at high load factors (ฮฑ = 90%โ95%), RSQF organizes the table into 64-slot blocks aligned to 64-byte cache lines and stores two 64-bit metadata bitmaps per block: occupieds and runends (reducing metadata overhead from 3 bits to 2.125 bits per slot, including block offset counters).
Using x86 hardware bit-manipulation instructions (POPCNT for rank and BMI2 PDEP / TZCNT for select), RSQF locates the precise slot where the run for quotient fq ends in O(1) constant instruction cycles even at 95% occupancy:
The Counting Quotient Filter (CQF) further encodes variable-length multiplicity counters directly inside each run using empty remainder slots when an item appears multiple times, outperforming the CountโMin sketch on skewed Zipfian distributions in network telemetry and bioinformatics k-mer counting (Squeakr) alongside de Bruijn graph assembly indexes.
Frequently Asked Questions
How can a quotient filter double its capacity without re-hashing the original keys?
Each stored entry in a quotient filter with parameters (q, r) implicitly preserves the full p-bit fingerprint f = (fq โช r) | fr, where p = q + r. To double the number of buckets from 2q to 2q+1, a single sequential scan extracts each (fq, fr) pair, shifts the top bit of the remainder fr into the bottom bit of the new (q + 1)-bit quotient, and writes the remaining (r โ 1)-bit remainder into the new table in sorted order.
Why does a Quotient Filter outperform a Bloom filter on SSD and NVMe flash storage?
In a flash-backed storage engine (such as a Bฮต-tree or LSM-tree), merging two SSTable runs with standard Bloom filters requires either keeping all raw keys to rebuild a larger Bloom filter or querying a stack of separate Bloom filters. Because quotient filters can be merged in a single sequential streaming pass over the filters alone, compaction never needs to re-hash keys or perform random I/O seeks.
References
- Cleary, J. G. (1984). "Compact hash tables using bidirectional linear probing." IEEE Transactions on Computers, C-33(9), 828โ834. https://doi.org/10.1109/TC.1984.1676499
- Bender, M. A., Farach-Colton, M., Johnson, R., Kraner, R., Kuszmaul, B. C., Medjedovic, D., Montes, P., Shetty, P., Spillane, R. P., & Zadok, E. (2012). "Don't thrash: How to cache your hash on flash." Proceedings of the VLDB Endowment, 5(11), 1627โ1637. https://doi.org/10.14778/2350229.2350275
- Pandey, P., Bender, M. A., Johnson, R., & Patro, R. (2017). "A general-purpose counting filter: Making every bit count." Proceedings of the 2017 ACM International Conference on Management of Data (SIGMOD '17), 775โ787. https://doi.org/10.1145/3035918.3035963
- Pandey, P., Bender, M. A., Johnson, R., & Patro, R. (2018). "Squeakr: An precise and approximate k-mer counting system." Bioinformatics, 34(4), 568โ575. https://doi.org/10.1093/bioinformatics/btx636
- Conway, A., Farach-Colton, M., & Shilane, P. (2018). "Optimal hashing in external memory." Proceedings of the 45th International Colloquium on Automata, Languages, and Programming (ICALP 2018), LIPIcs Vol. 107, 39:1โ39:14. https://doi.org/10.4230/LIPIcs.ICALP.2018.39