- Posted on
- admin
- No Comments
ClickHouse Architecture Explained
A ClickHouse query that aggregates a few billion rows in under a second looks like a magic trick from the outside. From the inside it’s closer to a very well organized warehouse: data arrives in small sorted crates, a background crew keeps combining those crates into bigger ones, a tiny catalog tells every query which shelves to ignore, and a fleet of worker threads sweeps the remaining shelves in tight, predictable loops.
If you’re new to ClickHouse, start with our ClickHouse tutorial for beginners, which covers parts, granules, the sparse index, and a hands-on Docker quickstart. This piece assumes that foundation and goes deeper into what’s actually on disk, how a query becomes a pipeline of threads, how replication and sharding really work, and how ClickHouse Cloud rethinks the whole storage model.
The Big Picture: Three Layers and a Shared-Nothing Cluster
One useful way to hold the whole system in your head is as three layers stacked on top of each other. (cite index=”118-1″>ClickHouse architecture is commonly described in three layers: a query processing layer, a data storage layer, and an integration layer built on table engines. The query layer turns SQL into work. The storage layer decides how bytes live on disk. The engine layer decides where data comes from and where it goes, whether that’s a local MergeTree table, a remote shard, or an external system.
At cluster scale, the design is deliberately simple. (cite index=”123-1″>ClickHouse uses a shared-nothing architecture, where each node has its own storage and operates independently. There’s no central storage array and no single coordinator that every query must pass through. Scaling out means adding nodes, and each node is a complete, self-sufficient database server.
SQL client
|
+---------v----------+
| Query processing | parse -> analyze -> plan -> pipeline
+---------+----------+
|
+---------v----------+
| Table engines | MergeTree | Replicated | Distributed | ...
+---------+----------+
|
+---------v----------+
| Storage (parts) | column files + marks + sparse index
+--------------------+
The Storage Layer: What a Part Actually Looks Like
Everything in ClickHouse’s storage story begins with the part. The beginner tutorial described parts as immutable sorted chunks, and now it’s worth opening one up.
Anatomy of a Part
A part is just a directory, and the files inside it tell you most of the architecture. (cite index=”125-1″>A typical part contains a compressed data file per column, a matching mark file per column that maps granule numbers to byte offsets, a primary.idx file holding the sparse index, plus small metadata files for row counts, column lists, checksums, and partition min/max values.
Two files per column matter most. (cite index=”127-1″>Each entry in a mark file holds two offsets: the byte position in the compressed .bin file where a granule’s block begins, and the position inside the decompressed block where the granule itself starts. The sparse index answers “which granules might match?” and the marks answer “where exactly on disk are those granules?”
Wide and Compact Parts
Not every part is laid out the same way. (cite index=”130-1″>Wide parts give each column its own pair of files, while compact parts bundle all columns into a single file, and the min_bytes_for_wide_part and min_rows_for_wide_part settings control where the switch happens. The reasoning is practical. (cite index=”130-1″>Small parts use the compact layout to avoid the filesystem overhead of thousands of tiny files, while large parts use the wide layout so a column scan reads only the columns it needs. You can see which layout a part uses in the part_type column of system.parts.
How a Read Actually Happens
Putting those pieces together gives you the precise sequence of a selective read. ClickHouse loads the small primary index into memory and binary-searches it to find candidate granules. It then consults the mark files to translate those granule numbers into byte offsets. Only then does it touch the .bin files, reading and decompressing just the blocks that contain the relevant granules, for just the columns the query references. (cite index=”126-1″>Because the lookup runs through in-memory indexes and marks, the I/O is deterministic and surgical rather than relying on page-cache tricks.
This is the reason a query touching 3 of 40 columns and 0.1 percent of the rows can finish so quickly. Nearly all of the work is deciding what not to read.
Compression and Codecs
Compression isn’t an afterthought in ClickHouse, it’s part of the storage design. Each column is compressed independently, and the values in a column tend to look alike, which compresses far better than a row-oriented layout where unrelated fields sit side by side.
(cite index=”131-1″>The protocol and storage support the generic LZ4 and ZSTD algorithms, with LZ4 being faster and ZSTD compressing more tightly. (cite index=”130-1″>LZ4 has historically been the default in open-source ClickHouse, while ClickHouse Cloud defaults to ZSTD(1).
The more interesting layer is the specialized codecs. (cite index=”129-1″>Delta stores differences between consecutive values, DoubleDelta stores second-order differences, Gorilla applies XOR-style compression suited to floating-point metrics, and T64 uses bit packing for integers. These are applied before the generic compressor, so you chain them. (cite index=”130-1″>A chained codec such as CODEC(Delta, ZSTD(1)) is read left to right: it computes deltas first and then compresses the result with ZSTD.
Why bother? Think about a timestamp column where each value is one second after the last. The raw values are large and unique, but the differences between them are nearly constant, so after a Delta or DoubleDelta step the data is almost all the same tiny number. (cite index=”130-1″>ZSTD on its own over a monotonic timestamp column typically compresses five to ten times worse than Delta combined with ZSTD.
A typical pairing looks like this:
CREATE TABLE metrics
(
ts DateTime CODEC(DoubleDelta, ZSTD(1)),
host LowCardinality(String),
value Float64 CODEC(Gorilla, ZSTD(1))
)
ENGINE = MergeTree
ORDER BY (host, ts);
LowCardinality deserves a mention here too. (cite index=”133-1″>Dictionary-style encodings like LowCardinality sit alongside the numeric codecs as type-aware transformations applied before general-purpose compression, which is why a country or status column costs almost nothing to store.
Merges: The Engine’s Quiet Background Workforce
Every insert creates a new part, so a busy table would drown in tiny parts without something to consolidate them. That something is the merge process. (cite index=”124-1″>Parts are merged in the background in a way similar to the LSM-tree principle, with smaller parts combined into larger ones to keep the disk layout efficient.
A few properties of merges are worth understanding:
- They’re heuristic. (cite index=”117-1″>Merges are triggered by heuristic algorithms that choose which parts to combine, rather than running on a fixed schedule. This is why a pile of small parts sometimes lingers for a while and then disappears.
- They preserve sort order. Because every part is already sorted by the sorting key, merging is mostly a streaming combine of sorted runs, which is cheap compared with a full re-sort.
- They’re where “special” engines do their work. ReplacingMergeTree deduplicates and SummingMergeTree adds up rows during a merge, which means the final result isn’t guaranteed until merging catches up.
- They never block readers. (cite index=”121-1″>A SELECT holds a snapshot of the table, meaning a set of parts, so a merge that swaps old parts for a new one doesn’t disturb a query already in flight.
That last point is the quiet elegance of immutability. Readers see a consistent set of parts, writers add new parts, and merges replace old parts with new ones atomically. Nothing is ever edited in place, so nothing needs a heavyweight lock.
The Query Processing Layer
Now to the other end of the system: how SQL text becomes work on your CPU cores.
From SQL to a Pipeline
ClickHouse parses a query into an abstract syntax tree, analyzes it against table metadata, builds a plan, and then turns that plan into an executable graph. (cite index=”117-1″>The execution flow parses the query, creates an interpreter, builds the execution plan, and manages logging and error handling, ultimately producing a processor graph that can run in parallel.
The storage engine participates directly in planning. (cite index=”121-1″>The query’s syntax tree is passed to the table engine’s read method, and the engine can use it to work out which indexes apply and to read less data from the table. This is why the sparse index, partition min/max values, and skipping indexes all kick in before any real I/O happens.
Vectorized Execution
The execution model is the same family of idea that makes DuckDB fast, which our DuckDB tutorial covers from the single-machine angle. (cite index=”121-1″>ClickHouse uses vectorized query execution, which lowers the cost of actual data processing and makes it easier to exploit CPU SIMD capabilities, with only limited support for runtime code generation.
In practice, operators don’t handle one row at a time. (cite index=”124-1″>Data moves through the engine in blocks, and processing batches of values reduces per-row overhead and improves CPU efficiency. A filter operator, for example, evaluates a whole column chunk in a tight loop that the CPU can pipeline and vectorize, rather than branching through function calls for each row.
Parallelism Across Threads
(cite index=”124-1″>During execution ClickHouse builds a physical operator graph and distributes the work across all available CPU cores, and it can repartition data between threads to balance the load and avoid idle resources. A single query on a single node therefore uses many cores, typically with different threads reading different granule ranges and then feeding partial results into aggregation stages.
You can inspect this directly with EXPLAIN PIPELINE, which prints the processor graph for a query. Seeing how many parallel streams a scan fans out into is one of the fastest ways to build intuition for how the engine spreads work.
Caches
(cite index=”133-1″>ClickHouse layers several caches, including the operating system page cache, mark and index caches, an uncompressed data cache in some workloads, compiled expression caches, and an opt-in query cache for repeated deterministic SELECT statements. The same source is blunt about their limits: caches help when queries repeat or touch the same marks and columns, but they can’t rescue a poor sorting key or a habit of selecting every column.
A Note on Joins
Honesty about weaknesses makes you a better architect. (cite index=”124-1″>ClickHouse isn’t optimized for complex multi-table JOINs in the way a traditional relational database is. Its sweet spot is scanning and aggregating one large table, optionally enriched with small dimension tables, dictionaries, or pre-joined data. Many ClickHouse schemas are deliberately denormalized for exactly this reason.
Skipping Work: Indexes, Projections, and Materialized Views
Because reading less is the whole game, ClickHouse gives you several layers of work avoidance.
- Partition pruning. Each part records min/max values for its partition key, so whole parts can be discarded cheaply.
- The sparse primary index. Covered in the tutorial, it selects granules by sorting-key prefix.
- Data skipping indexes. Optional secondary summaries over blocks of granules, useful for filters on non-key columns.
- Projections. (cite index=”120-1″>Projections let a table keep several alternative layouts or pre-aggregations, and the optimizer selects a matching one automatically with no query rewriting. (cite index=”120-1″>They’re rebuilt during background merges, which keeps them consistent with the base data but adds merge overhead.
- Materialized views. (cite index=”120-1″>Incremental materialized views update continuously as events arrive and write into a destination table you query directly, while refreshable views recompute from the source on a schedule.
The art of ClickHouse schema design is choosing which of these layers earns its keep for your workload, because every layer you add costs write amplification or merge time.
Replication: Keeper, Logs, and Identical Merges
A single node is a single point of failure, so production setups replicate. This is where the engine family splits. (cite index=”117-1″>StorageMergeTree handles standalone tables, while StorageReplicatedMergeTree adds replication coordinated through Keeper, which in practice means you pick the ReplicatedMergeTree engine and ClickHouse handles the rest.
The Keeper Role
(cite index=”118-1″>ClickHouse supports asynchronous multi-master replication for ReplicatedMergeTree tables, typically coordinated by ClickHouse Keeper, a C++ implementation of the Raft consensus algorithm. It plays the part ZooKeeper used to play, but it ships with ClickHouse itself. (cite index=”123-1″>Keeper coordinates replicas by tracking which data parts exist and keeping copies in sync, and it can run co-located with ClickHouse servers or as a separate deployment.
Notice what Keeper does not do. It doesn’t carry your data. It carries metadata and a replication log.
How Replicas Converge
(cite index=”118-1″>Each replica processes inserts and merges locally but coordinates through Keeper, converging by fetching missing parts or replaying operations logged there. (cite index=”121-1″>The logged actions include getting a part, merging parts, and dropping a partition.
Two details make this efficient. First, (cite index=”117-1″>replication is physical: only compressed parts are transferred between nodes, not the original queries or rows. A replica that needs data fetches a finished, compressed part rather than redoing the work. Second, (cite index=”121-1″>merges are coordinated between replicas so that the results are byte-identical, meaning a given set of parts is merged in the same way on every replica.
That coordination is why a replicated table behaves predictably. Any replica can accept an insert, and the others catch up by fetching the new part. There is no single “primary” to fail over.
The Replication Port
One operational footnote that surprises newcomers: (cite index=”119-1″>replication uses an additional port, 9009, for transferring parts between replicas. If replication stalls in a locked-down network, check that port early.
Sharding and the Distributed Table
Replication copies data for safety. Sharding splits data for scale. The two combine freely.
A Virtual Table Over Many Shards
(cite index=”119-1″>A Distributed table is a special table engine that stores no data itself, instead presenting a single interface over data spread across the cluster. (cite index=”123-1″>It sits on top of the underlying MergeTree or ReplicatedMergeTree tables on each shard, and querying it from any node makes that node fan the query out to all shards in parallel and merge the results.
What Happens During a Distributed Query
The sequence is worth walking through, because it explains both the speed and the limits. (cite index=”119-1″>ClickHouse rewrites the query to reference local table names, sends it to all shards in parallel, receives partial aggregation states back, merges them on the initiator node, and returns the final result. (cite index=”121-1″>Each remote server processes the query only up to the stage where intermediate results can be merged, and the initiating node then combines them.
This is why aggregations scale so well. Each shard does the heavy lifting on its own slice and ships back small partial states, such as per-group partial sums, rather than raw rows.
The Honest Limitation
The simplicity has a cost. (cite index=”121-1″>ClickHouse has no global query plan for distributed execution, only a simple one-pass approach of sending queries to remote nodes and merging the results. That makes subqueries and joins that involve two distributed tables tricky. (cite index=”119-1″>Depending on settings, an inner Distributed query inside a join can be executed in full on every shard of the outer query, which can become very expensive when the inner table is large. The usual cure is designing your data so that joined tables are colocated on the same shard key, or keeping the joined side small.
ClickHouse Cloud and SharedMergeTree
Everything above describes the classic, shared-nothing design where each replica keeps its own copy of the data on local disk. ClickHouse Cloud changes that.
(cite index=”122-1″>The SharedMergeTree table engine family powers ClickHouse Cloud. Instead of every replica holding and replicating its own bytes, the data lives once in shared object storage. (cite index=”122-1″>Replicas don’t talk to each other directly; all communication goes through shared storage and clickhouse-keeper, and SharedMergeTree implements asynchronous leaderless replication with Keeper handling coordination and metadata.
The payoff is operational. (cite index=”122-1″>This design leads to faster replication, mutation, merges, and scale-up operations, and it improves background merge throughput. Adding a replica no longer means copying terabytes, because the new node simply reads the same shared parts. (cite index=”122-1″>ClickHouse Cloud also uses a distributed query execution approach to apply more compute resources to a single query.
Conceptually, this is the same separation of storage and compute that data lake architectures pursue, and it connects nicely to the table-format world we’ve covered in our Apache Iceberg architecture guide. The difference is that SharedMergeTree keeps ClickHouse’s own part format and index structures rather than adopting an open table format.
Tracing One Insert and One Query End to End
Concrete traces make architecture stick, so here are both halves of the story.
An insert. A client sends a large batch. The server sorts it by the sorting key and writes a new part: compressed column files, mark files, a sparse index, and metadata. On a replicated table, the replica records the new part in Keeper’s log, and other replicas fetch the compressed part. Any materialized views fire and write to their destination tables as separate inserts. Later, a background merge folds this part into a bigger one, identically on every replica.
A query. A client sends SQL to any node. The node parses and plans it, asking the table engine how to prune. For a Distributed table, it rewrites the query for local tables and fans out to every shard. Each shard narrows to candidate parts, uses partition values and the sparse index to pick granules, reads only the needed columns through the mark files, decompresses blocks, and runs a vectorized pipeline across all its cores. Each shard returns partial aggregation states, and the initiator merges them into the final answer.
Every step reinforces the same theme: do less work, do it in batches, and spread what’s left across every core and node available.
Frequently Asked Questions
What are the main layers of ClickHouse architecture? A query processing layer that parses and executes SQL, a storage layer built on parts and the MergeTree engines, and an integration layer of table engines that connect to local storage, remote shards, and external systems.
What is stored inside a ClickHouse part? A compressed data file per column, a mark file per column mapping granules to byte offsets, a primary.idx sparse index, and small metadata files such as row counts, column lists, checksums, and partition min/max values.
What’s the difference between Wide and Compact parts? Wide parts store each column in its own files and suit larger parts where queries read only some columns. Compact parts store all columns in one file, which avoids creating many tiny files for small parts. Size thresholds decide which layout a part gets.
How does ClickHouse replication work? Replicated tables coordinate through ClickHouse Keeper, a Raft-based service. Each replica handles inserts and merges locally, logs actions in Keeper, and other replicas fetch the compressed parts. Replication is asynchronous and multi-master, so any replica can accept writes.
What does ClickHouse Keeper store? Metadata and a replication log, such as which parts exist and which actions each replica still needs to perform. It doesn’t store your table data.
What is a Distributed table? A virtual table that stores no data and routes queries to the underlying tables on each shard in parallel, then merges the partial results on the node that received the query.
Why aren’t JOINs ClickHouse’s strength? The engine is built to scan and aggregate large tables quickly, and distributed execution is a simple one-pass model with no global query plan. Joins across large distributed tables can be expensive, so schemas are often denormalized or arranged so joined data sits on the same shard.
What is SharedMergeTree? The table engine family behind ClickHouse Cloud. It keeps data in shared object storage and coordinates replicas through Keeper using leaderless replication, which speeds up scaling and merges compared with copying data between replicas.
Final Thoughts
ClickHouse’s performance isn’t one trick, it’s a stack of decisions that all point the same direction. Immutable sorted parts make writes cheap and reads consistent. Marks and a sparse index make reads selective. Column-wise compression with type-aware codecs shrinks what’s left. Vectorized pipelines spread the final work across every core. And shared-nothing sharding with Keeper-coordinated replication stretches the same design across a cluster.
Understanding those layers also tells you how to use the system well: batch your inserts, design the sorting key around your filters, treat joins with respect, and let merges do their job. For the authoritative reference, ClickHouse’s official architecture documentation is the best place to go deeper, and our ClickHouse tutorial for beginners is the place to start if you want to get hands-on first.
For more breakdowns of open-source data infrastructure and analytics tooling like this one, keep exploring the guides on CourseDrill.
Popular Courses
