sketches

Probabilistic

HyperLogLog, Count-Min Sketch, Cuckoo Filter — bounded-memory analytics.

What it is

Probabilistic on RedDB.

RedDB ships three first-class probabilistic data structures for real-time analytics workloads where exact counts are too expensive: HyperLogLog (count distinct), Count-Min Sketch (estimate frequency), and Cuckoo Filter (membership with delete). All three are SQL commands — no extension to install, no sidecar.

These structures answer "how many unique users today?", "how often did this event happen?", "have I seen this id before?" in fixed memory regardless of dataset size — the trade-off is bounded error you opt into per query.

Combined with the rest of the engine: pipe a time-series stream through a HyperLogLog to track unique-visitor cardinality per hour without storing every visitor; gate a hot path on a Cuckoo Filter to skip a database hit when membership is provably absent.

Code

Write it. Read it. Same engine.

Insert

HLL ADD distinct_users 'user:42';

Query

HLL COUNT distinct_users;

Use cases

Where probabilistic earn their place.

  • Unique visitor / DAU counters

    Bounded-memory cardinality across millions of users, mergeable across regions.

  • Top-K event frequency

    Track the most-frequent events / IPs / endpoints in fixed memory with sub-1% error.

  • Pre-flight membership checks

    Skip database lookups when the filter says "provably not present" — 99%+ negative answers stay in memory.

  • Abuse rate-limiting

    Sketches handle the long tail of distinct keys without blowing up your KV namespace.

Build it

End-to-end walkthrough.

Four steps from empty database to a real probabilistic workload — each step shows the exact code and explains what the engine is doing.

  1. Step 1

    1. HyperLogLog — count distinct in 16 KB

    A HyperLogLog at precision 14 holds ~16 KB and counts distinct elements with ~0.81% standard error — independent of dataset size. Merging is associative + commutative, so you can shard counters per region and combine without coordination.

    CREATE HLL distinct_users PRECISION 14;
    
    -- Stream of events
    HLL ADD distinct_users 'user:42';
    HLL ADD distinct_users 'user:99';
    HLL ADD distinct_users 'user:42'; -- duplicate, no extra cost
    
    HLL COUNT distinct_users;
    -- → 2  (≈ 0.81% error)
    
    -- Combine sketches across regions for a global estimate.
    HLL MERGE distinct_users_global FROM
      [distinct_users_us, distinct_users_eu, distinct_users_apac];
  2. Step 2

    2. Count-Min Sketch — top-K frequency

    Count-Min Sketch trades exactness for fixed memory — width × depth × 8 bytes regardless of how many keys flow through. Used for top-K queries, hot-key detection, abuse rate-limiting at scale.

    CREATE SKETCH event_frequency
      WIDTH 2048 DEPTH 5;
    
    SKETCH ADD event_frequency 'login.success' 1;
    SKETCH ADD event_frequency 'login.failed' 1;
    SKETCH ADD event_frequency 'login.success' 5;
    
    SKETCH COUNT event_frequency 'login.success';
    -- → ≈ 6  (overestimates by < 1/width with prob 1 - (1/2)^depth)
  3. Step 3

    3. Cuckoo Filter — membership with delete

    Cuckoo Filter answers "have I seen this id?" with a tiny false-positive rate and supports deletes (unlike Bloom filters). Use it as a cheap pre-check before a database lookup — most negative answers don't even hit storage.

    CREATE FILTER seen_payment_ids
      CAPACITY 1000000 FALSE_POSITIVE_RATE 0.001;
    
    FILTER ADD seen_payment_ids 'pi_12345';
    FILTER CHECK seen_payment_ids 'pi_12345';
    -- → true
    
    FILTER CHECK seen_payment_ids 'pi_99999';
    -- → false (provably absent — skip DB hit)
    
    FILTER DELETE seen_payment_ids 'pi_12345';
    -- Cuckoo supports delete; Bloom filters do not.
  4. Step 4

    4. Pipe a time-series into a sketch

    HLL_BUILD and HLL_CARDINALITY are aggregate-shaped UDFs. Plug into materialised views over time-series data — your weekly unique-visitor dashboard reads a tiny pre-computed sketch table, not the raw event stream.

    -- Continuous aggregate: hourly unique-visitor cardinality.
    CREATE MATERIALIZED VIEW mv_unique_visitors_hourly AS
      SELECT
        time_bucket('1 hour', timestamp) AS bucket,
        HLL_BUILD(tags->>'user_id')      AS sketch
      FROM page_views
      GROUP BY bucket;
    
    -- Query the view — sketches are materialised, counts are O(1).
    SELECT bucket, HLL_CARDINALITY(sketch) AS unique_users
    FROM mv_unique_visitors_hourly
    WHERE bucket > now() - INTERVAL '7 days'
    ORDER BY bucket DESC;

Engine features

What RedDB adds on top of probabilistic.

The engine knobs you would otherwise wire up yourself — TTL, encryption, indexes, atomic ops, eventual consistency — applied to this data model.

  • Three structures, three trade-offs

    HyperLogLog (count distinct, ~0.81% error, 16KB), Count-Min Sketch (frequency, configurable error vs memory), Cuckoo Filter (membership with delete, configurable false-positive rate).

  • Mergeable

    HLL and Count-Min sketches merge associatively. Track per-region, combine for a global estimate, no coordination overhead.

  • Aggregate UDFs

    HLL_BUILD, HLL_CARDINALITY, SKETCH_BUILD, FILTER_BUILD plug into GROUP BY and materialised views — same shape as SUM / AVG.

  • TTL eviction

    A sketch can have a TTL — sliding-window analytics ("unique visitors in the last hour") rebuild themselves automatically.

  • Durable + WAL-replicated

    Same durability + crash-recovery as tables. PITR captures sketch state at any wall-clock timestamp.

  • IAM policies apply

    A dev role can read sketch counts but not the underlying source events. Useful when the sketch summary is shareable but the raw stream is sensitive.

Showcases

What probabilistic can do once you bring the rest of the engine in.

Cross-model correlation, algorithm flex, multi-model patterns — things that take a stack of services elsewhere.

DAU dashboard at 1M+ users for free

A HyperLogLog at precision 14 counts daily-active-users in 16 KB. Per region. Mergeable.

Each region writes into its own HLL on every page view. A daily aggregator merges them. The DAU dashboard reads a single number per day per region — no event-table scan, no Redshift bill, no Spark job.

sql

-- Per-region writer (1 line per page-view).
HLL ADD dau_us 'user:${user_id}';

-- Aggregator (runs at end of day, or on demand).
HLL MERGE dau_global FROM [dau_us, dau_eu, dau_apac];

-- Dashboard query.
SELECT
  bucket,
  HLL_CARDINALITY(sketch) AS dau
FROM mv_dau_daily
WHERE bucket > now() - INTERVAL '90 days'
ORDER BY bucket;

Pre-flight Cuckoo Filter saves the database

API endpoint that processes Stripe events checks the filter before the DB lookup.

Stripe retries can produce duplicate events. Hitting webhook_events for every retry is wasteful. A Cuckoo Filter answers "have I seen this id?" in microseconds — 99%+ of negative answers never touch storage.

typescript

app.post('/webhooks/stripe', async (req) => {
  const evt = JSON.parse(req.rawBody)

  // Cheap pre-check.
  const seen = await db.query(
    "FILTER CHECK seen_stripe_events $1",
    [evt.id],
  )
  if (seen.rows[0].present) {
    return new Response('replay', { status: 200 })
  }

  // Real persist + record.
  await db.query("INSERT INTO webhook_events (id, payload) VALUES ($1, $2)",
    [evt.id, evt])
  await db.query("FILTER ADD seen_stripe_events $1", [evt.id])
  await processStripeEvent(evt)
  return new Response('ok')
})

Top-K abuse detection

Count-Min Sketch tracks the hottest IPs over a 5-minute sliding window, fires alerts on outliers.

Per-IP counters in KV would explode at high cardinality. A bounded-memory CMS over a 5-minute TTL captures the long tail without blowing up storage; an alert query asks "any IP whose estimated count is > 1000?".

sql

CREATE SKETCH ip_5min
  WIDTH 4096 DEPTH 5
  TTL 5m;

-- Per-request hook.
SKETCH ADD ip_5min '${request.ip}' 1;

-- Alert query (runs every 30s).
SELECT ip, count
FROM (
  SELECT
    distinct_ip                     AS ip,
    SKETCH_COUNT(ip_5min, distinct_ip) AS count
  FROM (SELECT DISTINCT tags->>'ip' AS distinct_ip FROM access_log
        WHERE timestamp > now() - INTERVAL '5 minutes') s
) t
WHERE count > 1000
ORDER BY count DESC;

Migrate from

Bring probabilistic over from your current tool.

From

Redis Stack (PFADD/PFCOUNT)

Redis HyperLogLog uses PFADD/PFCOUNT/PFMERGE. Translate 1:1 with a small driver shim.

redis-cli --scan --pattern 'pf:*' | while read k; do
  members=$(redis-cli HKEYS "$k")
  for m in $members; do
    red sql -d "reds://admin@.../my-db:5050" \
      "HLL ADD ${k#pf:} '$m';"
  done
done

From

Hand-rolled in app code

A naive Set<string> in your service rebuilt per request is the most common precursor — replace with a single HLL ADD on each event.

// Before — explodes in memory at 10M+ unique users.
const seen = new Set<string>()
events.on('view', e => seen.add(e.userId))

// After — fixed 16 KB.
events.on('view', e => db.query(
  "HLL ADD distinct_users $1", [e.userId]
))

Recipes

Common patterns, real driver code.

sql

Unique visitor counter (sliding window)

Sliding 24h DAU with a TTL-bound sketch. No per-row delete on event aging.

CREATE HLL dau_24h PRECISION 14 TTL 24h;
HLL ADD dau_24h ${user_id};
SELECT HLL_COUNT(dau_24h);  -- ~24h cardinality in 16 KB.

sql

Hot-key detector (frequency)

Count-Min Sketch over IPs in a 5-min window, alert on outliers.

CREATE SKETCH ip_freq WIDTH 4096 DEPTH 5 TTL 5m;
SKETCH ADD ip_freq ${ip} 1;
SKETCH COUNT ip_freq ${ip}; -- O(depth), ~5 hash probes.

typescript

Bloom-style pre-flight check

Cuckoo Filter saves a database round trip for the 99% of ids that are not present.

const present = await db.query(
  "FILTER CHECK seen_ids $1", [id]
)
if (!present.rows[0].present) {
  // safe to skip the DB hit — provably absent.
  return null
}
return await db.query("SELECT * FROM rows WHERE id = $1", [id])

Limits

What it does. What it costs.

Operational caps and durability semantics, no hand-waving.

HLL precision
4–18 (memory ranges 1KB–256KB; error 6.5%–0.04%)
Count-Min trade-off
Width controls error; depth controls confidence
Cuckoo capacity
Configurable; insert fails when 95%+ full
Mergeable structures
HLL, CMS — combine without coordination
Durability
Same WAL + fsync as tables

When NOT to use

Don't pick probabilistic for these.

Honest constraints — when another model fits better.

  • Don't use HLL for exact counts

    HLL is approximate by design — ~0.81% error at precision 14. If you need exact distinct counts (auditing, billing, regulated workloads), use a regular table with a UNIQUE index.

  • Don't use Cuckoo Filter for authoritative membership

    Cuckoo answers "probably yes" or "definitely no". Don't gate authorization on it; use it to skip negative-answer DB hits.

  • Avoid sketches when result-set retrieval is required

    Sketches store summaries, not the underlying elements. If you need the actual list of distinct items, you need a table or KV — not a sketch.

vs

How RedDB probabilistic compare.

Redis Stack (HLL/Cuckoo)

Same primitives, but joinable to your other data and durable through the same WAL. No second backup story.

Hand-rolled in app code

Engine-side merging is associative + concurrent-safe. App-side counters require shard coordination you do not want to write.

FAQ

Questions teams ask before they pick probabilistic.

How accurate is HyperLogLog?

Standard error is 1.04 / sqrt(2^precision). Precision 14 → ~0.81% error in 16 KB. Precision 18 → ~0.04% error in 256 KB. Pick based on your accuracy/memory trade-off.

Can I delete from a HyperLogLog?

No. HLL is add-only. For sliding windows, use a TTL on the sketch — old entries age out automatically.

How is Cuckoo different from a Bloom Filter?

Cuckoo supports delete; Bloom does not. Cuckoo also has slightly better space efficiency at typical false-positive rates. Trade-off: Cuckoo can fail to insert when full (~95% capacity).

Can sketches be sharded across machines?

Yes — HLL and CMS are mergeable. Per-shard sketches combine into a global one without coordination, which is exactly what makes them fit distributed analytics.

When should I use these instead of regular SQL aggregates?

When the cardinality is large enough that storing every element is wasteful — typically 100k+ distinct items per query window. Below that, a regular COUNT(DISTINCT) is exact and fast.

Try probabilistic on RedDB.

Free nano database, no credit card. AGPL self-host or managed Cloud — same engine, same file format.