Beyond the basics

The foundational ClickHouse lesson covered columnar storage, the MergeTree sparse index, parts and merges, and the engine family. This lesson assumes all of that and digs into the machinery that makes a production deployment fast and cheap: how columns are compressed, and the features that lean on good compression - materialized views, projections and data-skipping indexes, and SharedMergeTree cloud scaling.

Compression is the spine, so we start there. A columnar analytics engine is almost always limited by how many bytes it must pull off disk, so shrinking each column turns an I/O-bound scan into much cheaper CPU-bound work: the processor decompresses a small block instead of waiting on a large read. Everything else in this lesson is, in one way or another, about reading fewer bytes.

The effect is large in practice. On the public ClickBench benchmark ClickHouse typically stores data around 9-10x smaller than row-oriented databases, and production deployments often report 15-20x compression on real event and telemetry data (figures are illustrative and vary by workload). Less data on disk is the foundation every later feature builds on.

The compression pipeline has two stages

A ClickHouse column is compressed in two stages. Stage 1 is an optional column codec that rewrites the raw values into a lower-entropy form - storing differences, XOR-ing adjacent floats, or transposing integer bits. Stage 2 is a general-purpose compressor that squeezes that result down to the bytes that land on disk. You declare both in one CODEC(...) clause, and the chain is applied left-to-right on write and right-to-left on read.

Which general compressor runs by default depends on where ClickHouse runs: self-managed and open-source builds default to LZ4 (very fast, modest ratio), while ClickHouse Cloud defaults to ZSTD at level 1, trading a little CPU for noticeably smaller files. Either way the two stages work together: a well-chosen codec lowers the entropy so much that the general compressor wins big. Toggle the codecs below to see how each one reshapes the same column before that second stage runs.

Column codecs: transform, then compress

Pick a codec to see how stage 1 rewrites a column's values into a lower-entropy form before the general compressor (stage 2) runs. Delta stores differences from the previous value; DoubleDelta stores differences of differences (great for regular timestamps); Gorilla XORs adjacent floating-point values; T64 transposes a block of integers so shared high-order zero bits are stripped.

Illustrative values and bit counts - the point is the shape of each transform, not exact sizes. The column codec always feeds a second general-compression stage (LZ4 or ZSTD).

Choosing codecs and a compressor

So which codec, and which compressor? It depends entirely on the column. The general compressor is the broad choice: LZ4 decompresses very fast (several GB/s per core) with a modest ratio, while ZSTD is typically about 30% smaller on disk but roughly 2-3x slower to decompress, and it takes a level from 1 to 22 (e.g. ZSTD(3)). LZ4 is the self-managed default and ZSTD(1) the ClickHouse Cloud default. On top of that you stack a column codec matched to the data:

  • Delta / DoubleDelta for sorted, slowly-changing or monotonic integers and timestamps.
  • Gorilla, FPC, and ALP for floating-point columns: Gorilla XORs adjacent values and suits slow-moving gauges and metrics; FPC uses a predictor and fits correlated scientific or financial floats; and ALP (Adaptive Lossless floating-Point) is the newest, added in ClickHouse 26.3 LTS (March 2026).
  • T64 and GCD for integer columns with a narrow range (T64 bit-transposes a block, a frame-of-reference idea) or a common divisor (GCD divides one out).
  • LowCardinality - not strictly a codec but a column wrapper - dictionary-encodes columns with few distinct values (countries, statuses, device types), replacing repeated values with small integer keys. It pays off most when a column holds below roughly 10,000 distinct values per block.

Two things are close to free compression: a good ORDER BY and LowCardinality on repetitive columns. Sorting clusters similar values next to each other, so a low-cardinality prefix column compresses with near run-length efficiency and every codec has less to encode. The lab below combines a column profile, a codec stack, and the sort toggle so you can feel the trade-offs.

Compression lab

Choose a column profile and a codec stack to estimate bytes per row before and after compression, and toggle whether the column is sorted by the ORDER BY. A good ORDER BY plus the right codec can shrink a column many times over; truly random data (a UUID) resists every codec.

Ratios are illustrative, chosen to reflect each codec's real sweet spot. They model the two-stage pipeline: a column codec lowers entropy, then LZ4 or ZSTD compresses the result.

Materialized views: roll-ups on insert

The cheapest query is one you never run. A ClickHouse materialized view is insert-triggered and incremental: as rows land in the base table, the view computes a small roll-up and appends it to a target table - usually an AggregatingMergeTree. The view stores partial aggregate states with the -State combinators (sumState, uniqState, quantileState), and you finalise them at read time with the matching -Merge combinators. Because the maintenance happens incrementally at insert time, this is the scalable default for keeping aggregates fresh.

The payoff is that dashboard queries read a handful of pre-aggregated rows instead of scanning every event. Insert a few batches and compare the cost of a raw scan with the cost of reading the view.

Base table vs AggregatingMergeTree roll-up

Base table (events)

MV (events_daily, AggregatingMergeTree)

Illustrative event counts. The base table grows with every insert, while the materialized view keeps one roll-up row per day. A raw GROUP BY scans all event rows; reading the view merges a few stored states - so MV cost stays flat as data grows.

Projections and data-skipping indexes

The sparse primary index only helps filters on the sorting key. For everything else, ClickHouse has two ways to read fewer granules. Data-skipping indexes (minmax for ranges, set for low-cardinality equality, bloom_filter for high-cardinality equality) store a per-granule summary so the engine can skip granules that cannot match. Projections store the same data again in an alternate sort order or pre-aggregated, inside the table, and the optimiser picks the best one automatically.

Both cost extra storage and write amplification - the second copy or the index has to be written and merged too. Pick a query and watch which structure serves it.

The optimiser keeps getting sharper about projections: recent 26.x releases let aggregate projections be used through views, improved in-order and optimiser handling, and added a commit_order projection (26.4), so the planner picks a projection automatically in more cases than before.

Which structure serves the query?

Pick a query to see which structure serves it: a filter on the sort key uses the base sparse index; a filter on a non-sort column uses a data-skipping index (minmax / set / bloom_filter); and a query needing an alternate sort order or pre-aggregation uses a projection. Each alternate structure trades storage and write amplification for fewer granules read.

Illustrative granule counts (12 granules). Skipping indexes and projections both reduce granules read at the cost of extra storage and slower writes.

Scaling in the cloud: SharedMergeTree

For high availability on your own hardware, ReplicatedMergeTree keeps a full copy of the data on every replica and coordinates them through ClickHouse Keeper (the built-in ZooKeeper replacement). It is shared-nothing: N replicas means N copies of the data, and adding a replica means copying the whole dataset. It remains the right choice for self-managed, shared-nothing clusters.

ClickHouse Cloud changes the shape with SharedMergeTree, now GA and the default engine for every table type in Cloud (and in BYOC on AWS). It separates storage from compute: data lives once on object storage and stateless compute nodes attach to it. Replication is asynchronous and leaderless, coordinated through ClickHouse Keeper - and because Keeper holds only metadata, there is no replica-to-replica data copying. Storage stays flat no matter how many nodes you add, you can run hundreds of replicas, and you scale compute up or down with no resharding or rebalancing. Specialised counterparts exist too, such as SharedReplacingMergeTree.

New readers spin up instantly with no data copy, and parallel replicas split a single query across nodes - a trick both engines support. Toggle the engine and slide the replica count.

ReplicatedMergeTree vs SharedMergeTree

3 replicas

Toggle the engine and replica count. ReplicatedMergeTree is shared-nothing - each replica stores a full copy, so storage scales with the replica count and adding a replica means copying the data. SharedMergeTree keeps one copy on object storage and separates storage from compute, so storage stays flat, readers attach instantly, and parallel replicas speed a single query.

Illustrative storage and latency figures. Both engines support parallel replicas for single-query speed-ups; the difference is storage footprint and how quickly you can add compute.

More advanced features worth knowing

A few more capabilities round out a production deployment:

  • TTL tiering and recompression. A TTL clause can move old partitions to cheaper storage and recompress them - for example, keep recent data on fast disks with LZ4, then TTL ts + INTERVAL 30 DAY RECOMPRESS CODEC(ZSTD(12)) to shrink cold data with maximum-ratio ZSTD. The same mechanism can drop data once it expires.
  • Refreshable materialized views. Where incremental views react to each insert, a refreshable materialized view rebuilds on a schedule (REFRESH EVERY 1 HOUR or REFRESH AFTER) - useful for full-table recomputations or joins that cannot be maintained incrementally. They have been non-experimental since version 24.10, support an APPEND mode, can be chained with DEPENDS ON (dbt-style dependencies), and are monitored through system.view_refreshes.
  • Native vector search. ClickHouse has a built-in vector_similarity HNSW index for approximate-nearest-neighbour search in plain SQL, GA since version 25.8, with quantization (bf16/int8/binary) and pre- and post-filtering. It is its own topic - see the vector search and RAG lesson for how embeddings, ANN indexes, and retrieval fit together.

Together with compression, these features are why a single ClickHouse cluster can serve real-time dashboards, ad-hoc analytics, and increasingly semantic search from one copy of the data.

SQL recipes

The DDL and maintenance commands behind the ideas above. Per-column codecs, changing a codec in place, inspecting what compression actually achieved, an incremental roll-up, and a projection with a skipping index and TTL recompression.

-- Stack a column codec on the general compressor, per column.
-- Stage 1 = the column codec; stage 2 = the general compressor.
-- The second slot defaults to LZ4 (self-managed) or ZSTD(1) (ClickHouse Cloud).
CREATE TABLE events (
    ts       DateTime              CODEC(DoubleDelta, ZSTD(1)),  -- regular timestamps
    user_id  UInt32               CODEC(Delta, LZ4),            -- sorted, slowly changing
    price    Decimal(10, 2)       CODEC(T64, LZ4),              -- narrow integer range
    gauge    Float64              CODEC(Gorilla, ZSTD(1)),      -- slow-moving float
    reading  Float64              CODEC(ALP, ZSTD(1)),          -- ALP: adaptive float codec (26.3 LTS); FPC also fits floats
    country  LowCardinality(String),                            -- dictionary-encoded
    event    LowCardinality(String)
)
ENGINE = MergeTree
ORDER BY (toDate(ts), user_id)     -- sorting makes the codecs work harder
PARTITION BY toYYYYMM(ts)
SETTINGS index_granularity = 8192;

Key takeaways

  • Compression runs in two stages: an optional column codec lowers entropy, then a general compressor squeezes the result - LZ4 (the self-managed default) for speed, ZSTD (the ClickHouse Cloud default, at level 1) for size.
  • Match the codec to the column: Delta / DoubleDelta for sorted integers and timestamps, Gorilla, FPC, or ALP for floats, T64 / GCD for narrow-range integers, and LowCardinality for repetitive values.
  • A good ORDER BY and LowCardinality are close to free compression - sorted, low-cardinality columns give codecs far less to encode.
  • Materialized views (AggregatingMergeTree, -State / -Merge) keep roll-ups cheap; projections and data-skipping indexes read fewer granules at the cost of storage and write amplification.
  • SharedMergeTree is GA and the default engine in ClickHouse Cloud: it separates storage from compute, with one copy on object storage, stateless readers added instantly with no resharding, and leaderless replication coordinated by Keeper. ReplicatedMergeTree remains the self-managed, shared-nothing option.

Check your understanding

Five quick questions.

  1. 1. How does ClickHouse compress a column?
  2. 2. Which codec suits a column of regularly spaced timestamps?
  3. 3. Which codecs target floating-point columns?
  4. 4. What does LowCardinality do?
  5. 5. What is the key idea behind SharedMergeTree?