Why ClickHouse Is So Fast: 7 Design Choices Behind Its Speed

Unveiling the design choices behind the world's fastest real-time analytics DBMS

ClickHouse Performance Engineering · Deep-Dive Internals

Why ClickHouse Is So Fast: 7 Design Choices Behind Its Speed

Why ClickHouse is so fast is not a matter of one trick, but because every layer of the read path — column files, sparse primary index, skip indexes, per-column codecs, vectorized operators, multi-level parallelism and merge-time precomputation — was engineered to touch the fewest bytes and spend the fewest CPU cycles per row. This page walks through each mechanism with the DDL, EXPLAIN output and system.* tables you use to verify it on your own cluster.

By ChistaDATA Engineering
Updated August 2026
Verified against ClickHouse 26.3 LTS (latest stable 26.7)
Reading time ~14 min

Explore the 7 design choices
24×7 ClickHouse Support

8,192
Rows per granule — the unit the sparse primary index addresses (default index_granularity)
30+
Specialised hash-table implementations selected at runtime for GROUP BY (VLDB 2024 paper)
2
Phases of query latency — I/O and CPU — and ClickHouse is so fast because it attacks both
AVX-512
Runtime CPU dispatch selects SIMD kernels from SSE 4.2 up to AVX-512

Start here

Why ClickHouse is so fast: the question a practitioner actually asks

A team migrating an OLAP workload off Redshift, BigQuery, Elasticsearch or a row store sees ClickHouse return a GROUP BY over a few billion rows in sub-second time and asks the obvious question: what is it doing that our previous engine was not? The short answer to why ClickHouse is so fast is that it does dramatically less work per query. The long answer — and the useful one, because it tells you how to design tables that keep it fast — is below.

This article follows the query’s own path: first the I/O phase (which parts, granules and columns are read, and how many bytes come off disk), then the CPU phase (how those bytes are turned into a result). Each design choice is tied to the catalog table or EXPLAIN variant that lets you measure it, because a claim you cannot measure is not an engineering claim.

Scope. Everything here describes open-source ClickHouse (MergeTree family), verified against 26.3 LTS. ClickHouse Cloud’s SharedMergeTree shares the read-path mechanics but differs in storage layout and defaults (for example ZSTD rather than LZ4 as the default codec). Where behaviour is version-sensitive it is pinned inline.

Mental model

Query latency has two phases — ClickHouse attacks both

Take the canonical analytical query shape:

SELECT
    customer_id,
    sum(amount)               AS total_amount,
    quantile(0.95)(latency_ms) AS p95_latency
FROM events
WHERE event_date BETWEEN '2026-08-01' AND '2026-08-07'
  AND region = 'eu-west-1'
GROUP BY customer_id
ORDER BY total_amount DESC
LIMIT 100;

It touches three or four of perhaps several hundred columns, filters on a range predicate, aggregates, sorts, and truncates. Wall-clock time decomposes into Phase 1: locate and read the bytes (partition pruning → primary-index range selection → skip-index filtering → column-file reads → decompression) and Phase 2: process the bytes (vectorized filter, hash aggregation, partial sort, merge across threads and nodes).

In most warehouses Phase 1 dominates for cold, wide scans and Phase 2 dominates for hot, high-cardinality aggregations. Understanding why ClickHouse is so fast means looking at both: it is unusual in having been engineered aggressively at both. Design choices 1–4 below minimise Phase 1; choices 5–7 minimise Phase 2.

PHASE 1 · I/O — WHICH BYTES? Partitionpruning Primary indexgranule ranges Skip indexesdrop granules Read cols+ decompress system.parts EXPLAIN indexes=1 system.data_skipping_indices read_bytes PHASE 2 · CPU — HOW FAST? Vectorizedfilter / expr (SIMD) Hash aggregateper-thread states Merge + sortthreads → replicas ResultLIMIT 100 EXPLAIN PIPELINE ProfileEvents max_threads · parallel replicas query_duration_ms Choices 1–4 shrink Phase 1 · choices 5–7 shrink Phase 2 · all visible in system.query_log
Figure 1. The two-phase latency model behind why ClickHouse is so fast. Each stage names the catalog table or EXPLAIN variant that exposes it. Bytes eliminated in Phase 1 never cost CPU in Phase 2.

1

Columnar storage: read only the columns the query names

Why ClickHouse is so fast · reason 1 of 7

Every MergeTree part stores each column in its own compressed stream. In Wide parts that is a <column>.bin file plus a <column>.cmrk2 (or .mrk2) mark file per column; in Compact parts (small parts, below min_bytes_for_wide_part, default 10 MiB since 22.x) all columns share a single data.bin but remain separately addressable via marks. A query that references 4 of 300 columns opens 4 streams. The other 296 are never touched — no I/O, no decompression, no cache pollution.

The consequence for a row store is not subtle. PostgreSQL, MySQL/InnoDB and SQL Server keep a row’s columns together in a page; reading one column means reading the page that holds all of them. On a 300-column, 100-billion-row table the row store reads ~75× more bytes than ClickHouse for the same 4-column aggregate before any index or compression effect is counted. Redshift and Snowflake are also columnar, so this particular advantage is against OLTP engines and Elasticsearch/Lucene doc-values rather than against other warehouses — the differences against those come from choices 2–7.

Two properties make ClickHouse’s column layout more efficient than “columnar” alone implies: values in a column stream are fixed-width for numeric and date types with no per-value headers, and Nullable, LowCardinality, Array and Tuple types are decomposed into separate sub-streams (null-map, dictionary, offsets, elements) so that even a nested column is read only to the depth the query needs.

Design implication. Avoid SELECT * in production paths and avoid storing large JSON blobs as String when queries only need a few keys — since 25.3 the native JSON type stores each dynamic path as its own sub-column and prunes exactly like a regular column.

Why ClickHouse is so fast: row-store pages versus ClickHouse column streams in a MergeTree part
Figure 2. Columnar layout is the first reason why ClickHouse is so fast. Left: a row page must be read whole. Right: a Wide MergeTree part keeps one compressed stream per column; only streams named by the query are opened. Part-name format is <partition>_<min_block>_<max_block>_<level>; file names shown are the compressed-marks (.cmrk2) and compressed-primary-index (primary.cidx) variants produced when compress_marks and compress_primary_key are enabled (the default in current releases).

2

Sparse primary index: skip 99% of rows before reading a byte of data

Why ClickHouse is so fast · reason 2 of 7

Why ClickHouse is so fast: sparse primary index marks pointing to 8,192-row granules in a column file
Figure 3. Sparse primary index mechanics — the reason ClickHouse is so fast on sort-key range filters. Offsets in the mark row are illustrative; the structure (mark → compressed-block offset + offset within the decompressed block) is exact. Granule count per part is marks in system.parts.

A B-tree indexes every row; ClickHouse is so fast on range scans partly because its primary index instead stores the ORDER BY key for the first row of every granule — one entry per 8,192 rows by default (index_granularity; index_granularity_bytes = 10 MiB makes it adaptive for wide rows).

Because rows inside a part are physically sorted by that key, a predicate on the leading key columns turns into a binary search over the mark list, and the result is a set of granule ranges — not row pointers. The table’s other column files are then read only for those ranges, using the mark files to seek straight to the right compressed block. This granule-level pruning is the single largest contributor to why ClickHouse is so fast on range predicates.

The index lives in RAM (it is loaded on part activation; system.parts.primary_key_bytes_in_memory shows the footprint) and is trivially cheap to maintain because it is rewritten only when a part is written or merged, never updated in place. This is the reason ClickHouse can sustain millions of rows per second of ingest while keeping an index that filters reads — an insert only ever appends a new sorted part.

DDL that makes the index useful

CREATE TABLE events
(
    event_date   Date,
    region       LowCardinality(String),
    customer_id  UInt64,
    event_ts     DateTime64(3),
    amount       Decimal(18, 4),
    latency_ms   UInt32,
    payload      JSON
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (region, event_date, customer_id)
SETTINGS
    index_granularity       = 8192,
    index_granularity_bytes = 10485760;

Key order matters: put the lowest-cardinality, most-filtered column first (region), then the range column most queries constrain (event_date), then a higher-cardinality tiebreaker. The WHERE in Figure 1 uses region and event_date — a prefix of the key — so both predicates prune granules. A predicate on customer_id alone would not use the index efficiently (ClickHouse falls back to a generic exclusion search, which is weak on a non-prefix column). That is what skip indexes and projections are for.

Verify it: EXPLAIN indexes = 1

EXPLAIN indexes = 1
SELECT customer_id, sum(amount)
FROM events
WHERE event_date BETWEEN '2026-08-01' AND '2026-08-07'
  AND region = 'eu-west-1'
GROUP BY customer_id;

The output contains an Indexes: block for the ReadFromMergeTree step with, in order, MinMax (partition min/max), Partition (partition-key expression), PrimaryKey, and one entry per Skip index, each showing Parts: x/y and Granules: x/y before and after that index was applied. If PrimaryKey shows the same granule count before and after, the query is not using your ORDER BY and the table design needs to change, not the hardware.

3

Data-skipping indexes and partition pruning: filter on columns outside the sort key

Why ClickHouse is so fast · reason 3 of 7

Skip indexes are not row-level indexes. Each one stores a small summary per block of GRANULARITY n granules — a min/max pair, a set of distinct values, or a Bloom filter — and at query time ClickHouse uses that summary to prove that a block cannot contain matching rows and drops it.

They never produce false negatives; they simply cannot prune when the summary is inconclusive, which is why the value of a skip index is entirely a function of how the column’s values are physically clustered along the sort order. Used well, they extend why ClickHouse is so fast from the sort key to the next two or three most-filtered columns.

TypeSummary storedBest forWeak when
minmaxmin & max per blockColumns loosely correlated with the sort key (timestamps, monotonic IDs)Values uniformly spread through every block
set(N)up to N distinct values per blockLow-cardinality columns clustered in runsCardinality per block exceeds N (index becomes “unknown”)
bloom_filter(p)Bloom filter with false-positive rate pEquality / IN on high-cardinality values (IDs, UUIDs)Range predicates; values present in most blocks
text (current releases; supersedes the deprecated tokenbf_v1/ngrambf_v1)Inverted index over tokenshasToken, LIKE, full-text lookups on log linesVery short strings; extremely high ingest rates without tuning

Partition pruning sits one level above: the PARTITION BY expression’s min/max is stored per part (minmax_<col>.idx), so a predicate on the partition key eliminates whole parts before any granule-level work. Monthly partitions on event_date plus event_date in the sort key is the common pattern; do not partition finer than needed — thousands of small parts hurt merges and the “too many parts” guard far more than pruning helps.

Why this beats “clustering” in Snowflake/BigQuery. Both systems maintain micro-partition or block-level min/max statistics that serve the same purpose as minmax and partition pruning. The difference is that ClickHouse exposes the mechanism as explicit DDL with set, bloom_filter and text variants, keeps sort order deterministic at merge time, and lets you verify pruning per index in EXPLAIN. That is an engineering-control difference, not a magic-speed difference — and both vendors document their own approaches accurately.

Add a skip index and materialize it for existing parts

-- Bloom filter for point lookups on a high-cardinality column
-- that is NOT a prefix of ORDER BY.
ALTER TABLE events
    ADD INDEX idx_customer_bf customer_id
    TYPE bloom_filter(0.01)
    GRANULARITY 4;

-- Builds the index for parts that already exist. This is a
-- mutation: it rewrites index files, not data. Test on staging;
-- watch system.mutations for is_done = 1 before relying on it.
ALTER TABLE events MATERIALIZE INDEX idx_customer_bf;

Verify it: size and pruning

SELECT
    name,
    type_full,
    granularity,
    formatReadableSize(data_compressed_bytes) AS on_disk,
    marks
FROM system.data_skipping_indices
WHERE database = currentDatabase()
  AND table = 'events';

-- Then confirm the Skip entry in:
EXPLAIN indexes = 1
SELECT count() FROM events WHERE customer_id = 8812734;

If Granules after the Skip line are not materially lower than before it, the index is costing write amplification for nothing — drop it. A useful production rule: a skip index that prunes less than ~70% of granules for its target queries is usually the wrong tool; a projection with a different ORDER BY (choice 7) is the right one.

Related reading: how data-skipping indexes are implemented, parts, partitions and primary-index design, and the official skip-index guide.

4

Per-column compression codecs: fewer bytes off disk, decoded at memory speed

Why ClickHouse is so fast · reason 4 of 7

Codec chains per column

CREATE TABLE metrics
(
    ts        DateTime          CODEC(DoubleDelta, ZSTD(1)),
    series_id UInt64            CODEC(Delta(8), ZSTD(1)),
    host      LowCardinality(String),
    value     Float64           CODEC(Gorilla, ZSTD(1)),
    counter   UInt64            CODEC(T64, LZ4),
    raw_log   String            CODEC(ZSTD(3))
)
ENGINE = MergeTree
ORDER BY (series_id, ts);

Measure the ratio per column

SELECT
    column,
    any(compression_codec)                              AS codec,
    formatReadableSize(sum(column_data_compressed_bytes))   AS compressed,
    formatReadableSize(sum(column_data_uncompressed_bytes)) AS uncompressed,
    round(sum(column_data_uncompressed_bytes)
        / sum(column_data_compressed_bytes), 2)             AS ratio
FROM system.parts_columns
WHERE active
  AND database = currentDatabase()
  AND table = 'metrics'
GROUP BY column
ORDER BY sum(column_data_compressed_bytes) DESC;

Run this before and after a codec change on a copy of a real part. Never guess a codec: Gorilla on a slowly-varying gauge and on a random-walk float produce very different ratios, and T64 is only worth it on integers whose actual range is far below the declared width.

Because a column stream holds values of one type, often sorted or slowly varying, it compresses far better than a row page — and ClickHouse is so fast at reading it back because decompression runs at memory speed. ClickHouse compounds this with codec chains: a specialised codec that exploits data shape (Delta, DoubleDelta, Gorilla, T64, GCD, FPC, and in current releases ALP for floats — check the CREATE TABLE codec list for your exact version) followed by a general-purpose codec (LZ4 default in OSS, ZSTD default in Cloud, LZ4HC, and hardware-accelerated ZSTD_QAT/DEFLATE_QPL where the platform supports them).

The performance effect is twofold. First, fewer bytes are read: a 10× ratio turns a 1 TB scan into a 100 GB scan, and on object storage that is the difference between seconds and minutes. Second, decompression is faster than the I/O it replaces. LZ4 decodes at several GB/s per core; ZSTD level 1 is slower but still usually faster than NVMe on a per-core basis when the read is spread across max_threads. The break-even moves with hardware, which is why the choice is per-column and measurable, not global. Compression is the least-discussed part of why ClickHouse is so fast, and often the cheapest to improve.

Two structural details matter more than the codec list. The mark file records both the compressed offset and the offset inside the decompressed block, so a granule read seeks directly to one compressed block (default up to 1 MiB, max_compress_block_size) rather than decompressing a whole column. And LowCardinality(String) is not a codec but a dictionary-encoded column type: it stores an index stream plus a dictionary, so GROUP BY region aggregates over small integers, not strings — a Phase-2 win that comes from a Phase-1 storage decision.

Production caveat. Changing a codec via ALTER TABLE … MODIFY COLUMN rewrites the column in every part through a mutation. On a multi-TB table that is hours of background I/O and temporary double storage. Stage it, verify with system.mutations, and keep a tested restore path.

Deeper treatment: compression algorithms and codecs in ClickHouse.

5

Vectorized execution: operators work on column chunks, not rows

Why ClickHouse is so fast · reason 5 of 7

Once bytes are in memory, ClickHouse never reconstructs rows. The execution engine follows the MonetDB/X100 model: every operator (FilterTransform, ExpressionTransform, AggregatingTransform, MergeSortingTransform …) consumes and produces a chunk — a set of columns holding up to max_block_size rows (default 65,409). A filter evaluates region = 'eu-west-1' over 65 K values in a tight loop on a contiguous array, producing a UInt8 mask; the mask is then applied to the other columns with a single pass. This is the CPU-side half of why ClickHouse is so fast.

Three CPU-level effects follow. Instruction cache: the loop body is a few hundred bytes executed tens of thousands of times, so branch prediction and the µop cache work as designed. SIMD: hot kernels (arithmetic, comparisons, hashing, memcpy-like moves, aggregate state updates) are compiled in multiple variants and selected at runtime by CPU dispatch — SSE 4.2, AVX2, AVX-512 — without a separate build. Virtual-call amortisation: the interpreter overhead of choosing which typed implementation to run is paid once per chunk, not once per row.

Since 22.x ClickHouse can additionally JIT-compile chains of scalar expressions and aggregate-state updates with LLVM (compile_expressions, compile_aggregate_expressions, on by default after a query shape has been seen min_count_to_compile_expression times), fusing adjacent operators into one native loop. Its benefit is real on expression-heavy queries and negligible on I/O-bound ones — measure with ProfileEvents['CompileFunction'] before crediting it.

See the processors: EXPLAIN PIPELINE

EXPLAIN PIPELINE graph = 0
SELECT customer_id, sum(amount)
FROM events
WHERE event_date BETWEEN '2026-08-01' AND '2026-08-07'
GROUP BY customer_id
SETTINGS max_threads = 8;

Read it bottom-up: MergeTreeSelect × 8 (one stream per thread over disjoint mark ranges) → ExpressionTransform × 8AggregatingTransform × 8 (each thread builds its own hash table) → Resize → a merging step that combines the eight partial states. The × N multipliers are the intra-node parallelism of choice 6 made visible. Our step-by-step guide to reading EXPLAIN PIPELINE covers what each processor means.

ClickHouse vectorized execution: a column chunk flowing through Filter, Expression and Aggregating transforms
Figure 4. Vectorized execution, the CPU-side reason ClickHouse is so fast: a chunk of typed column arrays passes through processors; each processor runs one specialised loop per chunk. Dispatch cost is amortised over ~65 K rows instead of paid per row.

6

Parallelism at three levels: cores, shards, and parallel replicas

Why ClickHouse is so fast · reason 6 of 7

Three levels of ClickHouse parallelism: threads per replica, parallel replicas per shard, shards per cluster
Figure 5. Three multiplicative levels of parallelism — why ClickHouse is so fast on a cluster, not only on one node. Level 3 (parallel replicas) lets replicas that hold identical data split a single query by mark range — the mechanism ClickHouse Cloud relies on and that OSS clusters can enable per the parallel-replicas guide.

Most warehouses parallelise across nodes. ClickHouse is so fast under load because it parallelises at three multiplicative levels, and — critically — the unit of work at every level is the same object: a range of marks in a part. On clusters, this is why ClickHouse is so fast at scale rather than only on a single large node.

Level 1 — threads within a node. A single SELECT spreads its mark ranges over max_threads (defaults to the physical core count). Each thread runs its own copy of the pipeline in Figure 4 and builds a private aggregation hash table; states are merged at the end. Because the partial states live in a shared arena, ClickHouse can also convert to a two-level hash table mid-query when cardinality grows, so merging is bucket-parallel rather than serial.

Level 2 — shards. A Distributed table fans the query out to every shard, which executes it locally on its own data and returns partial aggregate states (not rows) to the initiator. With distributed_aggregation_memory_efficient the merge is streamed and bounded in memory.

Level 3 — parallel replicas. Introduced in 23.x, hardened through 24.x–25.x and the default machinery in ClickHouse Cloud: replicas of the same shard can cooperate on one query: the initiator hands out mark ranges dynamically and each replica reads a disjoint slice. This is what turns “a replica is a warm standby” into “a replica is compute”. Enable it with enable_parallel_replicas = 1, cluster_for_parallel_replicas, max_parallel_replicas; it requires the new analyzer (enable_analyzer = 1, the default since 24.3).

Blast radius. Raising max_threads or enabling parallel replicas increases per-query memory (one hash table per thread) and can starve concurrent queries. Set max_memory_usage and max_concurrent_queries in the same change, stage it on one replica, and confirm with system.query_log.memory_usage at p99 before rolling out. Sharding, distributed tables and parallel replicas covers the topology decisions in depth.

7

Merge-time precomputation: materialized views, projections and caches

Why ClickHouse is so fast · reason 7 of 7

The fastest query is one whose answer already exists — ClickHouse is so fast on dashboards largely because it precomputes. ClickHouse’s LSM-style storage — immutable parts merged in the background — gives it a place to do work between ingest and query that row stores do not have. Three mechanisms use it, and together they complete the picture of why ClickHouse is so fast for repeated dashboard and reporting queries.

Incremental materialized views

An insert trigger: every block inserted into the source table is run through the view’s SELECT and the result written to a target table, typically SummingMergeTree or AggregatingMergeTree. The target’s merges then fold partial aggregate states together, so a dashboard query reads hundreds of pre-aggregated rows instead of billions of raw ones. Since 24.x refreshable materialized views add scheduled full recomputation for cases where incremental semantics do not fit.

Projections

A hidden copy of the table (or a pre-aggregation of it) stored inside each part with a different ORDER BY. The optimiser picks the projection automatically when it would read fewer marks. Maintained by the same insert/merge path, so it is transparent to writers — at the cost of write amplification and storage. The right answer to “my second-most-common filter column is not in the sort key”.

Query cache & page caches

use_query_cache = 1 (since 23.x) stores whole result sets keyed by the normalised AST, with TTL and per-user isolation — safe for repeated dashboard queries, not for anything with now() (see how the query cache is implemented). Beneath it, the OS page cache, the mark cache, the uncompressed cache and the object-storage filesystem cache (S3/Azure/GCS tiers) each remove a layer of I/O for hot data.

Aggregating materialized view pattern

CREATE TABLE events_daily_by_customer
(
    event_date   Date,
    customer_id  UInt64,
    total_amount AggregateFunction(sum, Decimal(18, 4)),
    p95_latency  AggregateFunction(quantile(0.95), UInt32),
    event_count  AggregateFunction(count)
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(event_date)
ORDER BY (event_date, customer_id);

CREATE MATERIALIZED VIEW mv_events_daily_by_customer
TO events_daily_by_customer
AS
SELECT
    event_date,
    customer_id,
    sumState(amount)                 AS total_amount,
    quantileState(0.95)(latency_ms)  AS p95_latency,
    countState()                     AS event_count
FROM events
GROUP BY event_date, customer_id;

-- Query with -Merge combinators; GROUP BY must match target ORDER BY.
SELECT
    customer_id,
    sumMerge(total_amount)          AS total_amount,
    quantileMerge(0.95)(p95_latency) AS p95_latency
FROM events_daily_by_customer
WHERE event_date BETWEEN '2026-08-01' AND '2026-08-07'
GROUP BY customer_id
ORDER BY total_amount DESC
LIMIT 100;

Projection for a non-key filter column

ALTER TABLE events
    ADD PROJECTION prj_events_by_customer
    (
        SELECT *
        ORDER BY (customer_id, event_date)
    );

-- Builds the projection for existing parts (a mutation; stage it).
ALTER TABLE events MATERIALIZE PROJECTION prj_events_by_customer;

-- Confirm the optimiser chose it: look for the projection
-- name in the ReadFromMergeTree description.
EXPLAIN indexes = 1
SELECT count() FROM events WHERE customer_id = 8812734;

-- Or after the fact, from the query log:
SELECT query_id, projections, read_rows, read_bytes
FROM system.query_log
WHERE type = 'QueryFinish'
  AND has(tables, concat(currentDatabase(), '.events'))
ORDER BY event_time DESC
LIMIT 20;

Projections are incompatible with lightweight deletes/updates and cannot carry a WHERE or JOIN in their definition (26.3). Where those constraints bite, a materialized view into a second table is the fallback. Our projections guide and chained materialized views post go further; the official reference is the incremental materialized view and projections documentation.

Beyond the seven

Why ClickHouse is so fast at the micro level: the engineering that compounds the design

The seven choices above are the architecture — the structural answer to why ClickHouse is so fast. What separates ClickHouse from other engines with the same architecture on paper is the volume of specialised implementations behind each operator, all selected at runtime. The VLDB 2024 paper documents this in detail; the items below are the ones that show up most often in profiles.

  • 30+ hash-table variants for GROUP BY. Key type (fixed-width integers, one string, several strings, nullable, low-cardinality) and cardinality estimate select among specialised tables — including a two-level table that is converted to on the fly when a threshold is crossed so that the final merge across threads is parallel per bucket.
  • Adaptive JOIN algorithms. hash, parallel_hash, grace_hash, partial_merge, full_sorting_merge and direct (dictionary/KV lookups) are all available; join_algorithm = 'auto' starts with hash and spills to a merge join when the right-hand side exceeds memory limits. Recent analyzer releases also reorder joins and push predicates through them.
  • Multiple sort implementations. Partial sorting with LIMIT pushdown, radix sort for numeric keys, and optimize_read_in_order which streams already-sorted parts through ORDER BY without a sort step when the sort matches the table key.
  • Table-engine specialisation. ReplacingMergeTree, SummingMergeTree, AggregatingMergeTree, CollapsingMergeTree and VersionedCollapsingMergeTree move deduplication and roll-up logic into merges. For ingest-heavy key-value workloads ChistaDATA runs EmbeddedRocksDB alongside MergeTree, using RocksDB’s LSM as the write-optimised path.
  • Concurrency without shared locks. Reads see a consistent snapshot of the active part set via multi-versioning; inserts create new parts; merges replace parts atomically. There is no buffer-pool latch contention of the kind that limits OLTP engines under mixed load, which is why a single node comfortably serves hundreds of concurrent analytical sessions given adequate memory.

Related settings — current defaults on 26.3, per query or profile

SettingDefaultEffect on speed
max_threadsphysical coresLevel-1 parallelism; memory scales with it
max_block_size65 409Rows per chunk in the pipeline
compile_expressions1LLVM JIT for expression chains
join_algorithmdirect,parallel_hash,hashJoin strategy selection order
optimize_read_in_order1Skip sort when ORDER BY matches key
use_query_cache0Result-set caching (opt-in)
enable_parallel_replicas0Level-3 parallelism (opt-in in OSS)
enable_analyzer1New query analyzer/planner

Verify defaults for your build with SELECT name, value, changed FROM system.settings WHERE name IN (…). Session-level settings apply on the next query; server-level settings in config.xml (e.g. cache sizes) require a restart.

Honest edges

Where ClickHouse is not fast — and what to do about it

Every design choice above is a trade. Recognising the workloads on the wrong side of those trades is what keeps a ClickHouse deployment fast in year three, not just in the proof of concept. Understanding why ClickHouse is so fast also tells you exactly where it is not.

Point lookups & row-level updates

A primary key that addresses 8,192 rows at a time is the wrong tool for “fetch one row by ID”. Lightweight UPDATE/DELETE (25.x+) are usable but are still mutations under the hood. Keep OLTP in PostgreSQL/MySQL and stream changes in via CDC (Debezium, Kafka engine, ClickPipes-style connectors).

Large JOINs on the right-hand side

Hash joins build the right table in memory per query. A billion-row fact-to-fact join without denormalisation, dictionaries or grace_hash will exhaust max_memory_usage. Model for star schemas with dictionaries or pre-joined materialized views.

Many tiny inserts

Each INSERT creates a part; thousands per second overwhelm merges and trip parts_to_throw_insert. Batch at the client, or enable async_insert = 1 so the server batches for you. This is an ingestion-design issue, not a read-path one.

Vendor-neutral note: if your workload is primarily the first column, ClickHouse is the wrong engine and we will say so — MinervaDB’s PostgreSQL and MySQL practices exist for exactly that split. For a structured comparison, see the CTO’s guide to columnstores vs row stores.

Measurement

How to measure why ClickHouse is so fast on your own cluster

Every claim on this page about why ClickHouse is so fast maps to a column in system.query_log. The query below is the one we run first in every ChistaDATA performance engagement: it shows, per query, how many bytes Phase 1 let through and how much CPU Phase 2 spent on them.

SELECT
    normalizedQueryHash(query)                          AS query_hash,
    count()                                             AS executions,
    quantile(0.95)(query_duration_ms)                   AS p95_ms,
    formatReadableSize(avg(read_bytes))                 AS avg_read,
    avg(read_rows)                                      AS avg_read_rows,
    avg(ProfileEvents['SelectedMarks'])                AS avg_marks_selected,
    avg(ProfileEvents['SelectedParts'])                AS avg_parts_selected,
    avg(ProfileEvents['OSCPUVirtualTimeMicroseconds']) / 1000
                                                        AS avg_cpu_ms,
    formatReadableSize(max(memory_usage))               AS max_memory,
    any(query)                                          AS sample_query
FROM system.query_log
WHERE type = 'QueryFinish'
  AND event_time >= now() - INTERVAL 24 HOUR
  AND query_kind = 'Select'
GROUP BY query_hash
ORDER BY p95_ms DESC
LIMIT 25;

How to read it

SignalPoints atFix belongs to
avg_marks_selected ≈ total marks in tablePrimary index not usedChoice 2 — reorder ORDER BY
High read_bytes, few read_rowsWide columns or poor codecChoices 1 & 4
High avg_cpu_msp95_msGood parallelism, CPU-boundChoice 7 — precompute
p95_msavg_cpu_msI/O or lock/merge waitStorage tier, system.merges
max_memory near max_memory_usageHash table / join spill riskChoice 6 settings, join algorithm

Complement it with system.parts (part count, granule count, compression), system.merges (background pressure), system.trace_log with query_profiler_cpu_time_period_ns for flame graphs, and EXPLAIN PIPELINE for processor-level shape. That is the entire toolkit; there is nothing hidden.

Benchmarks. Independent, reproducible numbers: ClickBench (methodology and raw results per engine), and ChistaDATA’s published studies on ClickHouse on OpenTelemetry data and ClickHouse vs BigQuery. ClickHouse Inc.’s own ClickHouse vs Snowflake comparison is a vendor document and should be read as one.

FAQ

Why ClickHouse is so fast — common questions

Why ClickHouse is so fast — is it because it is in-memory?

No. ClickHouse is a disk-based (increasingly object-storage-based) columnar database. ClickHouse is so fast because it reads a small fraction of the stored bytes (columnar layout, sparse primary index, skip indexes, compression) and processes them with vectorized, multi-threaded operators. Memory is used for caches and aggregation state, not as the primary store.

Why ClickHouse is so fast compared with PostgreSQL or MySQL for analytics?

Row stores keep all of a row’s columns together and index individual rows, which is ideal for OLTP and wrong for wide-table aggregation. ClickHouse is so fast by comparison because it reads only the referenced columns, addresses data in 8,192-row granules, and executes chunk-at-a-time. On the same hardware the byte volume read per analytical query is typically one to two orders of magnitude lower. For transactional workloads the comparison reverses.

How does ClickHouse compare to Snowflake, BigQuery or Redshift on speed?

All four are columnar and parallel, so the reasons ClickHouse is so fast against them are narrower than against row stores. ClickHouse’s distinctive advantages are the explicit sparse primary index and skip indexes (deterministic, DDL-controlled pruning), merge-time precomputation (materialized views and projections inside the storage engine), and a runtime with many specialised operator implementations. Whether that translates into a lower latency for your workload is a measurement question — ClickBench publishes the methodology; run it against your own queries.

Does ClickHouse Cloud share the same reasons why ClickHouse is so fast?

Yes for the read path. ClickHouse Cloud’s SharedMergeTree separates compute from object storage and relies heavily on parallel replicas and the filesystem cache, and defaults to ZSTD compression. The primary-index, skip-index, vectorization and materialized-view mechanics are identical to open-source ClickHouse, which is what ChistaDATA operates for customers on their own infrastructure.

What is the single most important design decision for ClickHouse performance?

The ORDER BY key of each MergeTree table — most of what makes ClickHouse so fast flows from it. It determines what the primary index can prune, how well columns compress, whether optimize_read_in_order applies, and which projections you will later need. Get it right from query patterns before loading data; changing it afterwards means rebuilding the table.

Which ClickHouse version does this explanation of why ClickHouse is so fast describe?

Behaviour and defaults were verified against ClickHouse 26.3 LTS (the current long-term-support line, alongside 25.8 LTS) with 26.7 the latest stable release at the time of writing (August 2026). Version-sensitive features are pinned inline.

ChistaDATA · Full-stack ClickHouse infrastructure operations

Knowing why ClickHouse is so fast is the start. Keeping it fast in production is engineering.

ChistaDATA provides 24×7×365 consultative support, performance engineering, platform engineering and managed services for 100% open-source ClickHouse — with SLA-backed response times (S1 within 15 minutes) and zero vendor lock-in. If ClickHouse is so fast by design but your p95 is not where this page says it should be, we will find out why, with the same system tables shown above.

Talk to a ClickHouse engineer
ClickHouse Managed Services

ChistaDATA Inc. is committed to open-source software and building high-performance columnstores. ChistaDATA Corporation is not affiliated with ClickHouse Corporation. ClickHouse® is a registered trademark of ClickHouse, Inc. Behaviour described here was verified against open-source ClickHouse 26.3 LTS; test every change in staging before applying it to production and maintain a tested disaster-recovery posture. Historical reference for the OLTP/OLAP split: Stonebraker & Rowe, The Design of POSTGRES (1986).

About ChistaDATA Inc. 250 Articles
We are an full-stack ClickHouse infrastructure operations Consulting, Support and Managed Services provider with core expertise in performance, scalability and data SRE. Based out of California, Our consulting and support engineering team operates out of San Francisco, Vancouver, London, Germany, Russia, Ukraine, Australia, Singapore and India to deliver 24*7 enterprise-class consultative support and managed services. We operate very closely with some of the largest and planet-scale internet properties like PayPal, Garmin, Honda cars IoT project, Viacom, National Geographic, Nike, Morgan Stanley, American Express Travel, VISA, Netflix, PRADA, Blue Dart, Carlsberg, Sony, Unilever etc