Pulse 2025 Product Roundup: From Monitoring to AI-Native Control Plane

Read more

ClickHouse Approximate Methods: Trading Accuracy for Speed at Scale

Analytics at scale often doesn't require exact answers. When you're counting unique users across billions of events, computing percentiles for latency monitoring, or finding the most frequent values in a log stream, an answer that's 98% accurate and arrives in milliseconds is far more useful than an exact answer that takes minutes and exhausts your memory.

ClickHouse provides a rich set of approximate aggregation functions purpose-built for this trade-off. This guide covers what's available, how each method works, and when to use them.

Approximate Distinct Counting

Counting unique values is the most common — and most expensive — approximate operation. ClickHouse provides the standard `count(DISTINCT)` syntax, which by default computes an exact count. But for high-cardinality columns, the approximate alternatives are dramatically faster and lighter on memory.

uniq()

SELECT uniq(user_id) FROM events;

Uses an adaptive sampling algorithm. ClickHouse automatically selects the best internal algorithm based on the data:

  • For small sets: exact counting
  • For larger sets: approximate algorithm with typically less than 2% error

Memory usage: Significantly lower than exact counting for high-cardinality columns.

Best for: General-purpose approximate counting. This is the recommended default when exact counts aren't required.

uniqCombined()

SELECT uniqCombined(user_id) FROM events;

Uses a hybrid approach combining three algorithms:

  1. Array for very small cardinalities
  2. Hash table for medium cardinalities
  3. HyperLogLog for high cardinalities

Provides approximately 2–3% error for high-cardinality data and can optionally accept a precision parameter:

-- Higher precision (more memory, less error)
SELECT uniqCombined(17)(user_id) FROM events;

The precision parameter (default 17) sets the number of HyperLogLog cells as 2^precision. Higher values = more accuracy, more memory.

A 64-bit variant, uniqCombined64(), uses 64-bit hashes for better accuracy at extremely high cardinalities (billions of unique values) where 32-bit hash collisions become significant.

uniqHLL12()

SELECT uniqHLL12(user_id) FROM events;

Pure HyperLogLog implementation with 2^12 cells. Fixed memory usage regardless of cardinality (~2.5 KB per aggregation state). Error rate is approximately 1.6%.

Best for: Scenarios where you need a fixed, predictable memory footprint — especially useful in materialized views or AggregatingMergeTree tables where states are stored long-term.

uniqTheta()

SELECT uniqTheta(user_id) FROM events;

Based on the Theta Sketch algorithm from Apache DataSketches. Supports set operations (union, intersection, difference) on sketch states, which the HyperLogLog-based functions do not.

Best for: Advanced use cases requiring set operations on distinct count estimates — e.g., computing the overlap between two user segments without re-querying raw data.

Distinct Count Performance Comparison

For a column with ~100 million unique values:

Function Accuracy Memory per State Relative Speed
`count(DISTINCT)` / uniqExact Exact High (proportional to cardinality) 1x (baseline)
uniq ~2% error Low 3–5x faster
uniqCombined ~2–3% error Low–Medium 3–5x faster
uniqCombined64 ~2–3% error Medium 3–4x faster
uniqHLL12 ~1.6% error Fixed (~2.5 KB) 3–5x faster
uniqTheta ~2–3% error Medium 2–3x faster

The speed advantage grows with cardinality. At 10 billion unique values, approximate methods can be 10–50x faster than exact counting.

Configuring count(DISTINCT) to Use Approximate Methods

You can change which function `count(DISTINCT)` uses under the hood via the count_distinct_implementation setting:

-- Use approximate counting globally
SET count_distinct_implementation = 'uniq';
SELECT count(DISTINCT user_id) FROM events;

-- Or per-query
SELECT count(DISTINCT user_id) FROM events
SETTINGS count_distinct_implementation = 'uniqCombined';

This is useful when you want standard SQL syntax but don't need exact results, or when migrating from other databases where count(DISTINCT) was already approximate.

Approximate Quantiles and Percentiles

Exact percentile computation requires sorting the entire dataset — expensive for billions of rows. ClickHouse offers approximate alternatives.

quantileTDigest()

SELECT quantileTDigest(0.99)(response_time_ms) FROM requests;

Uses the t-digest algorithm, which maintains a compact sketch of the data distribution. Accuracy is highest at the tails (p99, p999) where it matters most for latency monitoring.

-- Multiple percentiles in one pass
SELECT
    quantilesTDigest(0.5, 0.9, 0.95, 0.99)(response_time_ms)
FROM requests;

Best for: Latency percentiles in monitoring dashboards, SLA reporting.

quantileTiming()

SELECT quantileTiming(0.95)(duration_ms) FROM events;

Optimized specifically for timing data (millisecond values). Uses a fixed-size histogram, making it very fast and memory-efficient but limited to values under ~30 seconds (30,000 ms). Values above the range are clamped.

Best for: Request latency and page load time monitoring where values are bounded.

quantileBFloat16()

SELECT quantileBFloat16(0.5)(value) FROM metrics;

Uses brain floating point format for reduced precision but very compact state. Suitable for rough estimates across massive datasets where memory is a constraint.

Top-K: Finding the Most Frequent Values

topK()

-- Find the 10 most common error messages
SELECT topK(10)(error_message) FROM logs;

Returns an approximate list of the most frequent values using the Space-Saving algorithm. Results are approximate — the exact order and inclusion of borderline values may vary.

-- With counts
SELECT topKWeighted(10)(url, hit_count) FROM page_views;

topKWeighted supports weighted counting, where each value contributes a specified weight rather than 1.

Best for: Finding hot paths, frequent errors, popular pages, or common query patterns.

Exact vs. Approximate Top-K

For exact results, you'd write:

SELECT error_message, count() AS c
FROM logs
GROUP BY error_message
ORDER BY c DESC
LIMIT 10;

This requires a full GROUP BY across all distinct values. topK() avoids materializing the full group-by result, using constant memory regardless of cardinality.

Sampling: Approximate Everything

ClickHouse supports SAMPLE clauses that process only a fraction of the data:

-- Process ~10% of data
SELECT
    count() * 10 AS estimated_total,
    uniq(user_id) AS approx_users
FROM events
SAMPLE 0.1;

-- Sample a fixed number of rows
SELECT avg(response_time) FROM requests SAMPLE 10000;

Sampling works at the granule level and requires a SAMPLE BY expression in the table definition:

CREATE TABLE events (
    ...
) ENGINE = MergeTree()
ORDER BY (event_date, cityHash64(user_id))
SAMPLE BY cityHash64(user_id);

Best for: Exploratory analysis on very large tables, or when any aggregation can tolerate proportional error. Sampling reduces I/O across all aggregation functions, not just specific ones.

Storing Approximate States in Materialized Views

One of the most powerful patterns is storing approximate aggregation states in AggregatingMergeTree tables for incremental computation:

CREATE TABLE daily_metrics
(
    date Date,
    unique_users AggregateFunction(uniq, UInt64),
    p99_latency AggregateFunction(quantileTDigest(0.99), Float64),
    top_urls AggregateFunction(topK(20), String)
)
ENGINE = AggregatingMergeTree()
ORDER BY date;

CREATE MATERIALIZED VIEW daily_metrics_mv
TO daily_metrics
AS
SELECT
    toDate(event_time) AS date,
    uniqState(user_id) AS unique_users,
    quantileTDigestState(0.99)(response_time) AS p99_latency,
    topKState(20)(url) AS top_urls
FROM events
GROUP BY date;

-- Query with merge
SELECT
    date,
    uniqMerge(unique_users) AS unique_users,
    quantileTDigestMerge(0.99)(p99_latency) AS p99_latency,
    topKMerge(20)(top_urls) AS top_urls
FROM daily_metrics
GROUP BY date;

The -State suffix captures the intermediate sketch, and -Merge combines sketches at query time. This lets you compute daily states during ingestion and roll them up into weekly or monthly views without re-scanning raw data:

-- Weekly unique users from daily pre-aggregated states
SELECT
    toStartOfWeek(date) AS week,
    uniqMerge(unique_users) AS weekly_uniques
FROM daily_metrics
GROUP BY week;

This correctly handles users who appear on multiple days — they're deduplicated during the merge. A simple SUM of daily distinct counts would overcount.

When to Use Approximate Methods

Situation Approach
Exact results required (billing, compliance) `count(DISTINCT)` / uniqExact / quantileExact
General analytics dashboards uniq, quantileTDigest, topK
Storing pre-aggregated states uniqCombined or uniqHLL12 with -State/-Merge
Billions of unique values uniqCombined64
Set operations on cardinality sketches uniqTheta
Fixed memory budget uniqHLL12
Exploratory analysis on huge tables SAMPLE clause
Latency monitoring (bounded values) quantileTiming
Finding hot paths or frequent values topK

Common Pitfalls

NULL Handling

All approximate functions exclude NULL values. If you need to count NULLs as a distinct value:

SELECT uniq(ifNull(user_id, 0)) FROM events;

Multi-Column Distinct Counts

You can count distinct combinations of multiple columns:

SELECT uniq(user_id, session_id) FROM events;

Results Vary Slightly Between Runs

Approximate algorithms use hashing, and the order of data processing across parallel threads can affect results. Variations are within the stated error bounds and are normal. If you need deterministic results, use exact methods.

Mixing Approximate and Exact

You can freely mix methods in the same query:

SELECT
    uniqExact(order_id) AS exact_orders,
    uniq(user_id) AS approx_users,
    quantileTDigest(0.99)(latency) AS approx_p99
FROM orders;

Frequently Asked Questions

Q: Is a 2% error acceptable for business metrics?

For most analytics — dashboards, trend analysis, audience sizing — yes. A 2% error on 1 million unique users means the count is between 980,000 and 1,020,000. For billing or compliance where exact counts are contractual, use exact methods.

Q: Do approximate functions work on distributed tables?

Yes, and this is where they excel. Each shard computes a compact sketch state that's sent to the coordinator for merging — far less network overhead than transferring millions of unique values for exact counting.

Q: Can I combine approximate states across different time granularities?

Yes. Store states at the finest granularity (e.g., hourly) and merge up to coarser granularities (daily, weekly, monthly) at query time using -Merge combinators. The sketches correctly handle deduplication across time periods.

Q: How do I know if approximate methods would help my queries?

Check system.query_log for queries with high memory_usage and read_bytes relative to the result size. High-cardinality GROUP BY or distinct count queries that use GBs of memory are prime candidates for approximate alternatives.

Q: Are there approximate methods for COUNT, SUM, or AVG?

COUNT, SUM, and AVG are already efficient and don't benefit from approximation in the same way. For these, use SAMPLE to reduce the data scanned instead. The approximate function family targets operations that are inherently expensive due to cardinality (distinct counts) or sorting (quantiles).

Pulse - Elasticsearch Operations Done Right

Pulse can solve your ClickHouse issues

Subscribe to the Pulse Newsletter

Get early access to new Pulse features, insightful blogs & exclusive events , webinars, and workshops.

We use cookies to provide an optimized user experience and understand our traffic. To learn more, read our use of cookies; otherwise, please choose 'Accept Cookies' to continue using our website.