Here is a rewritten version that applies the readability, focus, and retention improvements we discussed. It keeps your voice, all the concrete numbers, the Elixir examples, and the strong conceptual spine, while moving the payoff earlier and making the structure easier to scan and remember.


Data Sketches Beyond the One Billion Row Challenge

A data sketch is useful when a small, mergeable approximation is more valuable than an exact answer whose state is expensive to retain, move, or combine.

The One Billion Row Challenge is a good analytical story and a bad sketch tutorial if you stop at the original result. Gunnar Morling’s 1BRC asks for minimum, mean, and maximum temperature per weather station. That contract is exact. Sprinkling HyperLogLog on it does not make it more honest.

This article is about the questions around that contract: quantiles, distinct sensors, heavy hitters, set overlap, and rollups you can ship between regions. The sketches come from ex_data_sketch ~> 0.10. If you want an optimized Elixir 1BRC (file generation, parsing, profiling, schedulers), read Raj Rajhans’s elixir_1brc instead. This piece starts after the rows are already structured observations.

Roadmap

  1. When exact summaries win (and sketches add only error)
  2. The questions that actually need sketches
  3. HLL, KLL, and FrequentItems on a shared stream
  4. Short notes on Theta, CMS, and DDSketch
  5. A five-question decision rule and a final comparison

We assume records that already look like this:

%{station: "Abha", temperature: 18.4, sensor_id: "sensor-1042", region: "west"}

Estimates were run against ex_data_sketch 0.10.0 (compatible with ~> 0.9) on Elixir 1.18.4 / OTP 28 using the Pure backend. Timings are local observations, not a benchmark. The 12 000-row stream is illustrative of shape and merge behaviour; the real motivation appears at much larger cardinality and multi-node rollups.


When exact wins

Minimum and maximum are exactly mergeable: min(min_a, min_b) and max(max_a, max_b). Mean is exactly mergeable if you keep sum and count, not the mean itself. Per station that is four scalars:

%{min: -2.0, max: 22.0, sum: 30.0, count: 3}

With a few hundred station names the whole map stays tiny. Approximating it weakens the answer without solving a state-size problem.

QuestionBest structureExact or estimated?Why
Minimum by stationScalar accumulatorExactConstant state
Mean by stationSum and countExactExactly mergeable
Maximum by stationScalar accumulatorExactConstant state

That table is the conceptual anchor. Everything that follows is a different question.


Questions that need sketches

QuestionWhy exact state growsSketch
Median, p95, p99 temperatureRank statistics need ordered valuesKLL (or DDSketch for relative value error)
Distinct sensor IDsSet of IDs grows with cardinalityHLL
Dominant stations or alert codesFull frequency map grows with keysFrequentItems
Sensors seen in region A or region BExact sets grow; union stays boundedTheta (merge/2 is union)
Frequency of a specified error codePoint queries over a huge key spaceCMS

Each sketch returns an estimate with an explicit accuracy budget. Compatible sketches (same parameters, same hash identity where hashing is used) merge. Incompatible ones should refuse.


A deterministic telemetry stream

Twelve thousand already-structured observations, generated lazily in index order 0..11_999:

Exact reference values (computed independently of any sketch):

MeasurementExact
Rows12 000
Distinct sensors1 800
Distinct stations40
Median temperature (nearest-rank)10.0
p9528.0
p9929.6
Alert OK11 280
Alert HIGH_TEMP600
Alert SENSOR_FAULT120

Nearest-rank is an evaluation method for this sample, not a streaming algorithm.


Distinct sensors with HLL

Question: How many distinct sensor IDs have we seen?

HyperLogLog estimates cardinality. Precision p allocates m = 2^p registers. Relative standard error is about 1.04 / sqrt(m). You never store the IDs.

alias ExDataSketch.HLL

sensors = Enum.map(observations, & &1.sensor_id)

hll_p8  = HLL.new(p: 8)  |> HLL.update_many(sensors)
hll_p14 = HLL.new(p: 14) |> HLL.update_many(sensors)

HLL.estimate(hll_p8)   # ~1713.71
HLL.estimate(hll_p14)  # ~1787.01
ConfigEstimateAbs. errorRel. errorsize_bytesSerializedUpdate time
HLL p: 81713.7186.294.79 %26030034.1 ms
HLL p: 141787.0112.990.72 %16 38816 4286.0 ms

Region-local sketches with the same p: 14 merge to the identical estimate (1787.01). Compatible parameters are not optional.

Takeaway: Higher p buys tighter relative error at the cost of memory. The update-time difference above is a local observation — do not generalise it.

Use when: Distinct keys will not fit in memory or must be merged across partitions. Accept relative error of roughly 1.04 / √m.


Quantiles with KLL

Question: What is the median / p95 / p99 temperature?

A quantile is a value at a rank. Exact streaming quantiles want ordered observations. KLL keeps compact levels of samples and approximates rank. The parameter k trades memory for rank error (roughly 1.65 / k).

KLL’s guarantee is rank error, not “the number came out 0.2 °C off.” A 0.3 °C gap can be a small rank miss or a large one depending on the distribution.

alias ExDataSketch.KLL

temps = Enum.map(observations, & &1.temperature)
kll   = KLL.new(k: 200) |> KLL.update_many(temps)

KLL.quantiles(kll, [0.50, 0.95, 0.99])
MeasurementExactEstimateAbs. errorRank errorConfigSize / serialized
Median10.010.20.20.65 %k: 50748 / 790
p9528.028.00.00.02 %k: 50748 / 790
p9929.629.80.20.50 %k: 50748 / 790
Median10.010.30.30.90 %k: 2002 699 / 2 741
p9528.028.40.41.01 %k: 2002 699 / 2 741
p9929.629.80.20.50 %k: 2002 699 / 2 741

More memory buys a stronger statistical guarantee, not a monotonic improvement of every individual query. Exact min and max still belong in the four-scalar station summary — do not replace them with a quantile sketch.

Independent per-region k: 200 sketches merge cleanly (KLL.merge_many/1). Merged median was 10.0 versus the single-pass 10.3; internal compaction means results need not be bit-identical.

Use when: You need mergeable approximate distributions and can reason in rank error. Prefer DDSketch when you care about relative value error (typical for latency SLOs).


Heavy hitters with FrequentItems

Question: Which stations or alert codes dominate?

FrequentItems is SpaceSaving: at most k counters. Each tracked item carries an estimate and a maximum overcount (error). Low-frequency keys can be evicted.

When the key set is smaller than k, the sketch can match exact counts. On the three alert codes, k: 8 did exactly that:

ItemExactEstimateError boundLower–upper
OK11 28011 280011 280–11 280
HIGH_TEMP6006000600–600
SENSOR_FAULT1201200120–120

Serialized size: 158 bytes. Region-local sketches merged to the same three rows.

The instructive case is capacity k: 5 on the 40-station stream. Exact top counts were S0 = 1 200, S1 = 700, then several stations at 300. The sketch returned different keys with overcounts of 2 100–2 200. The true heavy hitters were gone. The error fields said so — if you read them.

FrequentItems answers “who looks hot, in bounded space?” It is not a frequency table. If you already know the key and need a point query, prefer CMS.

Use when: You can tolerate eviction of rare keys and want bounded counters with explicit overcount bounds. Never ignore the error fields.


Shorter cousins

Theta estimates set cardinality and supports union via merge/2. Union of the three regional sensor sketches estimated 1 713.07 distinct IDs against an exact 1 800. The public API used here is construction, update, union-merge, estimate, and serialize. Do not invent an intersect/2.

CMS (Count-Min Sketch) answers point queries over a large key space. On the alert stream, CMS.new(width: 256, depth: 3) recovered the exact counts for OK and HIGH_TEMP in 3 081 bytes of state.

DDSketch is the quantile cousin for relative value error (typical for latency SLOs). It wants non-negative values — shift temperatures or use it on latencies.


Error is a budget, not a vibe

Also:


What sketches actually optimise

They do not make parsing faster. They do not automatically beat min/max/sum/count on a 40-station map.

They do:

region A observations → sketch A ┐
region B observations → sketch B ├→ merge → global estimate
region C observations → sketch C ┘

Not retaining raw events can reduce how much identifiable payload you keep around. That is a retention property, not a security guarantee.


When not to use sketches

Give this list more weight than the happy path.

Sketches complement exact aggregates and warehouses. They do not automatically replace them.


A five-question decision

  1. What exact question must the summary answer?
    Min/mean/max is not a quantile, and neither is a distinct count.

  2. How does exact state grow with rows or distinct keys?
    Four scalars per station stay cheap. A set of sensor IDs does not.

  3. What error can consumers tolerate?
    Rank error, relative cardinality error, and overcount bounds are different sentences. Write the one you mean.

  4. Must summaries merge across partitions or time windows?
    If yes, you need compatible, associative merge — not a shared process that eats one message per row.

  5. Will future queries need information the sketch discards?
    If yes, keep a raw or exact path beside the sketch, or do not sketch.


Comparison at a glance

SketchAnswersMain parameterError typeMergeTypical size order
Exact 4-scalarmin / mean / maxnoneexactconstant
HLLdistinct countp (registers)relative cardinalityyeshundreds of bytes → tens of KB
KLLquantiles (rank)krank erroryeslow KB
FrequentItemsheavy hittersk (capacity)overcount + evictionyeshundreds of bytes
Thetacardinality + set unionrelative cardinalityunionsimilar to HLL
CMSpoint frequency querieswidth × depthovercountyeslow KB
DDSketchquantiles (relative value)relative accuracyrelative value erroryeslow KB

Process the billion rows. Retain only the state the questions justify.



What changed and why