use std::collections::HashSet;
use std::hash::{BuildHasher, BuildHasherDefault, Hasher};
pub const EXACT_CARDINALITY_THRESHOLD: usize = 1_000_000;
#[derive(Default)]
struct FingerprintHasher(u64);
impl Hasher for FingerprintHasher {
fn write(&mut self, bytes: &[u8]) {
for byte in bytes {
self.0 = self.0.rotate_left(8) ^ u64::from(*byte);
}
}
fn write_u64(&mut self, value: u64) {
self.0 = value;
}
fn finish(&self) -> u64 {
self.0
}
}
type Fingerprints = HashSet<u64, BuildHasherDefault<FingerprintHasher>>;
struct HllBuildHasher;
impl BuildHasher for HllBuildHasher {
type Hasher = std::collections::hash_map::DefaultHasher;
fn build_hasher(&self) -> Self::Hasher {
std::collections::hash_map::DefaultHasher::new()
}
}
#[derive(Clone)]
pub struct HyperLogLog {
registers: Vec<u8>,
}
impl std::fmt::Debug for HyperLogLog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HyperLogLog")
.field("precision", &Self::PRECISION)
.field("registers_len", &self.registers.len())
.finish()
}
}
impl HyperLogLog {
const PRECISION: usize = 14;
const NUM_REGISTERS: usize = 1 << Self::PRECISION;
pub fn new() -> Self {
Self {
registers: vec![0u8; Self::NUM_REGISTERS],
}
}
#[inline]
pub fn insert(&mut self, value: &str) {
self.insert_hash(Self::hash(value));
}
#[inline]
fn hash(value: &str) -> u64 {
let mut hasher = HllBuildHasher.build_hasher();
hasher.write(value.as_bytes());
hasher.finish()
}
#[inline]
fn insert_hash(&mut self, hash: u64) {
let index = (hash as usize) & (Self::NUM_REGISTERS - 1);
let window = hash >> Self::PRECISION;
let rank = (window.leading_zeros() - Self::PRECISION as u32 + 1) as u8;
if rank > self.registers[index] {
self.registers[index] = rank;
}
}
pub fn count(&self) -> u64 {
let register_count = Self::NUM_REGISTERS as f64;
let alpha = 0.7213 / (1.0 + 1.079 / register_count);
let raw_estimate: f64 = alpha * register_count * register_count
/ self
.registers
.iter()
.map(|®ister| 2.0_f64.powi(-(register as i32)))
.sum::<f64>();
if raw_estimate <= 2.5 * register_count {
let zeros = self
.registers
.iter()
.filter(|&®ister| register == 0)
.count() as f64;
if zeros > 0.0 {
(register_count * (register_count / zeros).ln()) as u64
} else {
raw_estimate as u64
}
} else if raw_estimate <= (1u64 << 32) as f64 / 30.0 {
raw_estimate as u64
} else {
let two32 = (1u64 << 32) as f64;
(-two32 * (1.0 - raw_estimate / two32).ln()) as u64
}
}
pub fn merge(&mut self, other: &HyperLogLog) {
for (left, right) in self.registers.iter_mut().zip(other.registers.iter()) {
*left = (*left).max(*right);
}
}
pub fn memory_usage_bytes(&self) -> usize {
self.registers.len()
}
}
impl Default for HyperLogLog {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CardinalityEstimator {
exact: Option<Fingerprints>,
hll: HyperLogLog,
threshold: usize,
inserted: u64,
floor: usize,
}
impl CardinalityEstimator {
pub fn new() -> Self {
Self::with_threshold(EXACT_CARDINALITY_THRESHOLD)
}
pub fn with_threshold(threshold: usize) -> Self {
Self {
exact: Some(Fingerprints::default()),
hll: HyperLogLog::new(),
threshold,
inserted: 0,
floor: 0,
}
}
#[inline]
pub fn insert(&mut self, value: &str) {
self.inserted += 1;
let hash = HyperLogLog::hash(value);
self.hll.insert_hash(hash);
if let Some(exact) = self.exact.as_mut() {
exact.insert(hash);
self.spill_if_over_budget();
}
}
#[inline]
pub fn insert_owned(&mut self, value: String) {
self.insert(&value);
}
pub fn spill(&mut self) {
self.floor = self.distinct_lower_bound();
self.exact = None;
}
#[inline]
fn spill_if_over_budget(&mut self) {
if self
.exact
.as_ref()
.is_some_and(|exact| exact.len() > self.threshold)
{
self.spill();
}
}
pub fn estimate(&self) -> usize {
match &self.exact {
Some(exact) => exact.len(),
None => (self.hll.count() as usize)
.min(usize::try_from(self.inserted).unwrap_or(usize::MAX))
.max(self.floor),
}
}
pub fn is_approximate(&self) -> bool {
self.exact.is_none()
}
pub fn distinct_lower_bound(&self) -> usize {
match &self.exact {
Some(exact) => exact.len(),
None => self.floor,
}
}
pub fn merge(&mut self, other: &CardinalityEstimator) {
self.inserted += other.inserted;
self.hll.merge(&other.hll);
let floor = self
.distinct_lower_bound()
.max(other.distinct_lower_bound());
match (self.exact.as_mut(), other.exact.as_ref()) {
(Some(mine), Some(theirs)) => {
mine.extend(theirs.iter().cloned());
if mine.len() > self.threshold {
self.spill();
}
}
_ => {
self.exact = None;
self.floor = floor;
}
}
}
pub fn memory_usage_bytes(&self) -> usize {
std::mem::size_of::<Self>() + self.hll.memory_usage_bytes() + self.exact_bytes()
}
pub fn exact_bytes(&self) -> usize {
self.exact
.as_ref()
.map_or(0, |set| hash_table_bytes::<u64>(set.capacity()))
}
}
fn hash_table_bytes<T>(capacity: usize) -> usize {
const GROUP_WIDTH: usize = 16;
if capacity == 0 {
return 0;
}
let buckets = if capacity < 8 {
capacity + 1
} else {
capacity / 7 * 8
}
.next_power_of_two();
buckets * (std::mem::size_of::<T>() + 1) + GROUP_WIDTH
}
pub fn spill_largest_exact_sets(
estimators: &mut [&mut CardinalityEstimator],
budget: usize,
) -> usize {
let mut held: usize = estimators
.iter()
.map(|estimator| estimator.exact_bytes())
.sum();
if held <= budget {
return held;
}
let mut by_size: Vec<(usize, usize)> = estimators
.iter()
.map(|estimator| estimator.exact_bytes())
.enumerate()
.filter(|&(_, bytes)| bytes > 0)
.collect();
by_size.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
for (position, bytes) in by_size {
if held <= budget {
break;
}
estimators[position].spill();
held -= bytes;
}
held
}
impl Default for CardinalityEstimator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
const SPILL_AT: usize = 10_000;
fn estimator_over(distinct: usize) -> CardinalityEstimator {
fill(CardinalityEstimator::new(), distinct)
}
fn spilled_over(distinct: usize) -> CardinalityEstimator {
fill(CardinalityEstimator::with_threshold(SPILL_AT), distinct)
}
#[test]
fn the_distinct_floor_survives_every_way_the_exact_set_is_dropped() {
let over = spilled_over(SPILL_AT + 500);
assert!(over.is_approximate());
assert_eq!(over.distinct_lower_bound(), SPILL_AT + 1);
let mut pressured = estimator_over(1_234);
pressured.spill();
assert!(pressured.is_approximate());
assert_eq!(pressured.distinct_lower_bound(), 1_234);
pressured.spill();
assert_eq!(pressured.distinct_lower_bound(), 1_234);
assert_eq!(estimator_over(77).distinct_lower_bound(), 77);
}
#[test]
fn a_merge_keeps_the_larger_floor_or_the_union() {
let mut spilled = estimator_over(300);
spilled.spill();
let mut merged = estimator_over(900);
merged.merge(&spilled);
assert!(merged.is_approximate());
assert_eq!(merged.distinct_lower_bound(), 900);
let mut left = CardinalityEstimator::with_threshold(SPILL_AT);
let mut right = CardinalityEstimator::with_threshold(SPILL_AT);
for value in 0..8_000 {
left.insert(&value.to_string());
}
for value in 4_000..12_000 {
right.insert(&value.to_string());
}
left.merge(&right);
assert!(left.is_approximate());
assert_eq!(left.distinct_lower_bound(), 12_000);
}
#[test]
fn an_estimate_never_falls_below_the_floor() {
let mut undershoots = 0;
for set in 0..20 {
let mut est = CardinalityEstimator::with_threshold(SPILL_AT);
for value in 0..=SPILL_AT {
est.insert(&format!("{set}-{value}"));
}
assert!(est.is_approximate());
if (est.hll.count() as usize) < est.distinct_lower_bound() {
undershoots += 1;
}
assert!(
est.estimate() >= est.distinct_lower_bound(),
"set {set}: {} < {}",
est.estimate(),
est.distinct_lower_bound()
);
}
assert!(undershoots > 0, "no set made the sketch undershoot");
}
#[test]
fn the_floor_never_exceeds_the_true_distinct_count() {
let mut est = CardinalityEstimator::with_threshold(SPILL_AT);
for round in 0..3 {
for value in 0..(SPILL_AT + 10) {
est.insert(&format!("{value}"));
}
assert!(est.distinct_lower_bound() <= SPILL_AT + 10, "round {round}");
}
}
fn fill(mut est: CardinalityEstimator, distinct: usize) -> CardinalityEstimator {
for value in 0..distinct {
est.insert(&value.to_string());
}
est
}
#[test]
fn a_table_sized_column_stays_exact() {
for distinct in [50_000usize, 200_000] {
let est = estimator_over(distinct);
assert!(
!est.is_approximate(),
"{distinct} distinct should stay exact"
);
assert_eq!(est.estimate(), distinct);
}
}
#[test]
fn the_largest_sets_spill_first_and_only_until_under_budget() {
let mut small = estimator_over(1_000);
let mut large = estimator_over(50_000);
let mut tied_first = estimator_over(20_000);
let mut tied_second = estimator_over(20_000);
let large_bytes = large.exact_bytes();
let tie_bytes = tied_first.exact_bytes();
let small_bytes = small.exact_bytes();
assert!(large_bytes > tie_bytes && tie_bytes > small_bytes);
let budget = tie_bytes + small_bytes;
let held = spill_largest_exact_sets(
&mut [&mut small, &mut large, &mut tied_first, &mut tied_second],
budget,
);
assert_eq!(held, budget);
assert!(large.is_approximate());
assert!(tied_first.is_approximate());
assert!(!tied_second.is_approximate());
assert!(!small.is_approximate());
assert_eq!(small.estimate(), 1_000);
let mut kept = estimator_over(1_000);
let bytes = kept.exact_bytes();
assert_eq!(spill_largest_exact_sets(&mut [&mut kept], bytes), bytes);
assert!(!kept.is_approximate());
}
#[test]
fn table_bytes_count_the_buckets_behind_the_capacity() {
let mut set = Fingerprints::default();
let mut seen = Vec::new();
for value in 0..100_000u64 {
set.insert(value);
if seen.last() != Some(&set.capacity()) {
seen.push(set.capacity());
}
}
for capacity in seen {
let buckets = (hash_table_bytes::<u64>(capacity) - 16) / 9;
assert!(buckets.is_power_of_two(), "capacity {capacity}");
let expected = if buckets < 8 {
buckets - 1
} else {
buckets / 8 * 7
};
assert_eq!(capacity, expected, "{buckets} buckets");
}
assert_eq!(hash_table_bytes::<u64>(0), 0);
}
#[test]
fn a_spilled_estimate_never_exceeds_the_values_inserted() {
let est = spilled_over(50_000);
assert!(est.is_approximate());
assert!(
est.hll.count() > 50_000,
"the sketch no longer overshoots here; this test reaches nothing"
);
assert_eq!(est.estimate(), 50_000);
let mut merged = spilled_over(50_000);
merged.merge(&spilled_over(50_000));
assert!(merged.estimate() <= 100_000);
}
#[test]
fn small_columns_are_exact() {
for distinct in [0usize, 1, 999, 1_000, 1_001] {
let est = estimator_over(distinct);
assert!(
!est.is_approximate(),
"{distinct} distinct should stay exact"
);
assert_eq!(est.estimate(), distinct);
}
}
#[test]
fn exactly_at_threshold_is_still_exact() {
let est = spilled_over(SPILL_AT);
assert!(!est.is_approximate());
assert_eq!(est.estimate(), SPILL_AT);
assert!(spilled_over(SPILL_AT + 1).is_approximate());
}
#[test]
fn spilling_on_demand_keeps_an_honest_estimate() {
let mut est = estimator_over(20_000);
assert!(!est.is_approximate());
let exact_bytes = est.memory_usage_bytes();
est.spill();
assert!(est.is_approximate());
assert!(est.memory_usage_bytes() < exact_bytes);
let error = (est.estimate() as f64 - 20_000.0).abs() / 20_000.0;
assert!(error < 0.03, "spilled estimate off by {error:.4}");
}
#[test]
fn large_columns_estimate_without_the_hard_cap() {
for distinct in [SPILL_AT + 1, 100_000, 500_000] {
let est = spilled_over(distinct);
assert!(
est.is_approximate(),
"{distinct} distinct should spill to HLL"
);
let estimate = est.estimate();
assert_ne!(estimate, SPILL_AT);
let error = (estimate as f64 - distinct as f64).abs() / distinct as f64;
assert!(
error < 0.03,
"{distinct} distinct: estimate {estimate} off by {error:.4}"
);
}
}
#[test]
fn merge_of_exact_halves_stays_exact() {
let mut left = CardinalityEstimator::new();
let mut right = CardinalityEstimator::new();
for value in 0..400 {
left.insert(&value.to_string());
}
for value in 200..600 {
right.insert(&value.to_string());
}
left.merge(&right);
assert!(!left.is_approximate());
assert_eq!(left.estimate(), 600);
}
#[test]
fn merge_with_spilled_side_is_approximate() {
let mut small = estimator_over(10);
let big = spilled_over(100_000);
small.merge(&big);
assert!(small.is_approximate());
let error = (small.estimate() as f64 - 100_000.0).abs() / 100_000.0;
assert!(error < 0.03, "merged estimate off by {error:.4}");
}
#[test]
fn estimate_is_deterministic_across_runs() {
assert_eq!(
spilled_over(250_000).estimate(),
spilled_over(250_000).estimate()
);
}
}