Why ClickHouse is so fast is best answered with a number: on a two-core machine with 7 GB of memory, it averaged a column across 1,000,000,000 rows in 1.06 seconds, roughly 940 million rows per second, and answered a per-tenant question over the same billion rows in 8 milliseconds by reading 7 granules out of 122,127. Neither result needed a cluster, a cache or a ClickHouse performance tuning pass. Both follow from seven design choices that run through every layer of the engine, and together they are why ClickHouse is so fast at any scale we have measured.
This article measures each of those seven choices on ClickHouse 26.7 against a billion-row events table we generated for the purpose, with the relevant setting switched off wherever the engine allows it so that the contribution is isolated rather than asserted. It also places our lab numbers next to the published figures from the largest ClickHouse deployments in the world, clearly marked as theirs, because the point of understanding why ClickHouse is so fast is knowing how far the same mechanisms carry.
All lab figures: ClickHouse 26.7.2.1, 2 vCPU / 7 GB / local NVMe-class disk, median of three runs with use_query_condition_cache = 0 unless stated. The table is larger than memory, so these are ClickHouse performance numbers from disk, not warm-cache numbers.
The scale at which why ClickHouse is so fast actually matters
A billion rows is a useful laboratory size because it is large enough to exceed memory and small enough to rebuild in eight minutes. It is not large by ClickHouse standards, and why ClickHouse is so fast at a billion rows is only interesting because the same answer holds at a quadrillion. The published production figures below are the reference points we keep in mind when we design a cluster; they are the vendors’ and operators’ own numbers, cited to source, not ours.
Table 1. Published scale references beside the lab, the context in which ClickHouse performance claims should be read. Cloudflare and LogHouse figures are quoted from the linked ClickHouse posts and have not been independently verified by us.
Two of those figures deserve a pause. A 16x compression ratio at 431 PiB is design choice 4 operating on real telemetry rather than on our synthetic data, and 190 million rows per second of ingest is design choice 7’s insert path at fleet scale. Everything measured below is the same machinery at one ten-thousandth of the size, which is exactly why ClickHouse performance characteristics measured on a small box transfer so reliably to a large one, and why ClickHouse is so fast on both.
The billion-row table
The table models a multi-tenant SaaS event stream: 500 tenants, 20,000 users each, a six-month window, 17 event types with a realistic skew towards page views, 40 countries, 4 device classes, 300 URL paths, HTTP status codes at a 95% success rate, and latency and payload columns with a long tail. Every value is a deterministic hash of the row number, so anyone can regenerate the identical table and check every ClickHouse performance figure below to the row; the generator is in the appendix.
Three facts from that output shape everything that follows about why ClickHouse is so fast on this table. The table is 15.84 GiB against 7 GB of RAM, so full scans are reading from disk, not from cache. The primary index for a billion rows is 570 KiB, one entry per 8,192-row granule, and it is loaded lazily on first use. And the default LZ4 codec achieved only 1.47x on this deliberately random data, which is a floor we will raise to 2.68x in design choice 4 and which real telemetry, as LogHouse’s 16x shows, leaves far behind.
Design choice 1: one compressed stream per column
Every Wide MergeTree part stores each column as its own compressed file with its own mark file. A query names columns and pays for those columns only, which is the first and simplest reason why ClickHouse is so fast on wide tables. On this table the latency_ms column is 1.77 GiB of the 15.84 GiB; a query that averages it reads that 1.77 GiB and nothing else, and count() reads no column data at all because row counts live in part metadata.
Table 2. ClickHouse performance on full-table scans, two cores, table larger than memory. Wall-clock tracks the compressed size of the columns named, not the size of the table, which is the whole argument for the layout.
Read the rows-per-second column against the bytes column and the ClickHouse performance pattern is plain: the engine is moving roughly 0.6 to 1.7 GB of compressed data per second off a virtual disk and through decompression on two cores, and the query time is that transfer time. A row store holding the same billion rows would have to move all 23 GiB for any of these queries. Our columnar versus row-based measurement has the PostgreSQL side of that comparison on a 20-million-row copy: 3.85 GB of heap read for a two-column aggregate that ClickHouse served from 45.5 MiB.
Design choice 2: a sparse primary index that reads 7 granules out of 122,127
A B-tree carries one entry per row. ClickHouse carries one entry per granule, the first sort-key tuple of every 8,192 rows, which is why 570 KiB indexes a billion rows. Because rows inside a part are physically sorted by the key, a predicate on the leading key columns becomes a binary search over that list and the result is a set of granule ranges. The mark file then seeks each named column directly to the compressed block that holds the first selected granule.
Two layers cooperate here. Part-level column statistics, which the 26.7 build maintains automatically, discard 35 of the 40 parts because their event_time range cannot contain 10 March; the primary key then binary-searches the 4,873 granules of the five survivors down to 7. Eight milliseconds against a billion rows, and the table was never in memory. If one measurement explains why ClickHouse is so fast for tenant-scoped analytics, it is this one.
Table 3. Five predicates, one table, and the ClickHouse performance range they span. The same 106 rows cost 28 ms or 2,875 ms depending on whether the query also names the first sort-key column. Nothing else changed.
The last two rows are the most instructive thing on this page for anyone designing a schema. A predicate that follows the sort-key prefix is resolved by the index; a predicate that does not is a full scan of that column, however selective it is. This is the single largest lever in ClickHouse performance engineering, and it is set at CREATE TABLE time, which is why ClickHouse is so fast for teams that chose their ORDER BY carefully and merely adequate for teams that did not.
Reading the plan
If the PrimaryKey line of EXPLAIN indexes = 1 shows the same granule count before and after, the query is not using your ORDER BY. Fix the key, add a projection with a different order, or accept the scan. No server setting compensates for it.
Design choice 3: partition pruning, skip indexes and the query condition cache
Above the primary index sit coarser filters, each cheap, each visible in the same plan output that drives every ClickHouse performance review. The partition key’s min/max is kept per part, so a month predicate on this table touches 5 to 7 of the 40 parts before any granule-level work. Automatic column statistics, visible as the Statistics line in the plans above, do the same for columns that are not the partition key. Skip indexes extend the idea to columns outside the sort key altogether: a min/max pair, a set of values or a Bloom filter per block of granules, consulted to prove that a block cannot contain a match.
The 26.x line adds a further layer that surprised us during this lab. The query condition cache remembers, per predicate and per granule, which granules matched the last time the predicate ran. On the user_id scan from Table 3, the one with no index help at all, the second execution went from 2,020 ms to 19 ms with no change to the query, the table or the data.
A 100x improvement on a repeated dashboard filter costs nothing and is on by default in this build, which is why every headline number in this article was measured with it switched off. It is also why a naive benchmark that runs each query three times and reports the best run will overstate ClickHouse performance on filtered queries by two orders of magnitude. Report the first run, or disable the cache, and say which; every ClickHouse performance figure in this article does.
Table 4. Skip-index types on ClickHouse 26.x and their ClickHouse performance profile. On our 20-million-row sibling table a Bloom filter pruned 92% of granules and moved wall-clock by nothing, because PREWHERE had already done the work on a warm column; on the billion-row table, where the user_id column is 3.41 GiB and cold, it is precisely the tool for the 2,875 ms row of Table 3.
Design choice 4: per-column codecs, measured on 99.5 million rows
A column stream holds values of one type, often sorted or slowly varying, and ClickHouse lets each column declare its own codec chain: a shape-aware codec (Delta, DoubleDelta, Gorilla, T64, GCD, FPC) followed by a general-purpose one (LZ4 by default in open-source ClickHouse, ZSTD by default in ClickHouse Cloud). We copied the first 99.5 million rows of the events table into a second table with codecs chosen per column and compared, because compression is the least-discussed part of why ClickHouse is so fast and often the cheapest to improve.
Table 5. Codec effect per column, the ClickHouse performance lever that costs nothing at query time. event_time is the lesson: a timestamp column that LZ4 could not compress at all shrank 4.2x once told it was a sequence of deltas.
The codec table was also faster to query, not slower, a ClickHouse performance result that runs against the usual intuition about ZSTD. The same avg(latency_ms), count() ran in 155 ms on the ZSTD table against 186 ms on the LZ4 slice, and the 17-way GROUP BY in 358 ms against 413 ms.
On the 20-million-row table in our sibling article, which fitted in page cache, LZ4 won by 20% because the scan was CPU-bound; here the table does not fit and the scan is bound by bytes moved, so the smaller table wins. Which regime a production node is in is a measurement, not an opinion, and system.parts_columns on your own data is where the answer lives.
Design choice 5: vectorised execution, and what it costs to lose it
Once bytes are in memory ClickHouse never reconstructs rows, and this is where why ClickHouse is so fast stops being about I/O. Every processor consumes and produces a chunk of column arrays holding up to max_block_size rows, 65,409 by default, and runs one tight typed loop per chunk with SIMD kernels selected at runtime. The interpretive cost of deciding what to execute is paid once per chunk rather than once per row. The cheapest way to price that design, and to see how much of why ClickHouse is so fast lives in it, is to shrink the chunk on a billion rows.
Table 6. Why ClickHouse is so fast per byte: same query, same data, same two threads, same bytes read. At 1,024 rows per call the per-call overhead is 11 extra seconds on a billion rows. At the default it is amortised to nothing, which is a large part of why ClickHouse is so fast per byte.
PREWHERE is the vectorised engine’s I/O-side twin and a ClickHouse performance feature most teams inherit without knowing it. When a query filters on cheap columns and selects expensive ones, the optimiser reads the filter columns first, evaluates the predicate, and fetches the remaining columns only for granules that survived. We asked for every column of every row that was a purchase, returned HTTP 500 and took more than 1.4 seconds: 2,924 rows out of a billion.
Six seconds of ClickHouse performance from a default setting most teams have never heard of, on a query that reads 233 MiB of http_status to avoid reading 15 GiB of everything else. On a node with more cores and faster storage the absolute times shrink; the ratio does not.
Design choice 6: parallelism over mark ranges, from two cores to three clouds
ClickHouse parallelises at three multiplicative levels, and the unit of work at every level is the same object: a range of marks inside a part. A thread, a shard and a replica each take mark ranges; none of them introduces a new kind of coordination, which is why ClickHouse is so fast to scale out as well as up. On our two cores the first level is the only one we can show, and it is close to ideal; it is also why ClickHouse is so fast on a cluster for the same reason it is fast on one node.
Table 7. ClickHouse performance scaling across threads on identical mark ranges. Each thread runs a private pipeline and a private aggregation hash table; the two are merged once at the end.
The ClickHouse performance blast radius of turning any level up is memory: one aggregation hash table per thread, per replica. Raise max_threads or enable parallel replicas together with max_memory_usage and max_concurrent_queries, stage on one replica, and read system.query_log.memory_usage at p99 before rolling out.
Design choice 7: merge-time precomputation, 5,567 ms to 7 ms
MergeTree is LSM-shaped. Inserts create immutable sorted parts; background merges combine them, re-sort them, rebuild marks and indexes, apply TTL and recompress. That gives the engine a place to do work between ingest and query that a row store does not have, and incremental materialized views use it: every inserted block is run through the view’s SELECT and the resulting aggregate states are written to a target table whose merges fold states with the same key together.
The view is 1.4% of the table on disk and answers the two dashboard questions 16x and 795x faster respectively, including a distinct-user count that would otherwise hash half a billion values. Two disciplines keep this ClickHouse performance pattern honest in production. The reading query’s GROUP BY must match the target’s ORDER BY, and a view sees only new inserts, so existing data needs the one-time backfill shown and any later correction of source rows needs a matching correction in the target.
Granularity is the design decision, and the difference between a view that explains why ClickHouse is so fast on a dashboard and one that merely costs disk. Our first attempt was an hourly view keyed by tenant, hour and event type. It grew to 21.7 million rows and 2.12 GiB, carried a quantile state per row, and answered the quarterly question in 4,078 ms against 4,455 ms from the base table, a saving not worth its storage.
Collapsing to tenant and day, and dropping the quantile in favour of the count, error and distinct-user states the dashboard actually displays, produced the 271 ms result. A materialized view is a query plan frozen at insert time; it has to be the plan the dashboard needs, and when it is, it is why ClickHouse is so fast on dashboards that summarise billions of rows.
The merge is also where ingest design meets ClickHouse performance, and where why ClickHouse is so fast to read becomes a question of how it is written. One thousand single-row inserts took 2.3 s and produced one thousand parts, which background merges reduced to six, the ClickHouse performance failure mode that fills support queues; one insert of 10,000,000 rows took 0.17 s, a rate of 57 million rows per second into a sorted part.
LogHouse’s 190 million rows per second across 5,800 inserts per second is the same arithmetic at fleet size: about 33,000 rows per insert. Batch at the client or set async_insert = 1 so the server batches for you; parts_to_throw_insert (3,000 by default) is the guard rail that eventually rejects the alternative.
Where ClickHouse is not fast, on the same billion rows
Understanding why ClickHouse is so fast tells you exactly where it is not, and our lab produced three examples without looking for them. A point filter on a column outside the sort key is a full column scan: 2,875 ms for 106 rows in Table 3, against 28 ms when the query also names the tenant.
The second example is a ClickHouse performance edge inside the aggregation engine. A GROUP BY on two LowCardinality columns at once, country and device, took 25 seconds where the single-key GROUP BY took 3, because the aggregation falls off the fixed-width fast path onto a serialised composite key; the fix is a precomputed view or a single derived key, and it is the kind of thing a profile finds in a minute and a benchmark table never shows.
The third example is aggregation cost rather than I/O. A 95th-percentile latency across 160 country-device groups took 27 seconds because a billion values were sampled into 160 quantile states; that query belongs in a materialized view, not on a dashboard.
The structural limits of ClickHouse performance are the ones we measured on the 20-million-row sibling table in the row-store comparison: a point lookup by primary key at 5 to 9 ms against 0.1 ms in PostgreSQL, and a single-row UPDATE as an 8-second mutation against 0.65 ms. Hash joins build the right-hand table in memory per query, so a billion-row fact-to-fact join without dictionaries, denormalisation or grace_hash exhausts max_memory_usage. If a workload is mostly point reads and row-level writes, no ClickHouse performance tuning rescues it; it belongs on a row store with CDC into ClickHouse for the analytical half, and MinervaDB’s PostgreSQL and MySQL practices exist for exactly that split.
Measuring why ClickHouse is so fast, or is not, on your own cluster
Every claim on this page maps to a column in system.query_log. This is the first query we run in a ChistaDATA ClickHouse performance engagement, per query shape: how many bytes Phase 1 let through and how much CPU Phase 2 spent on them.
Table 8. Reading query-log output back to the seven design choices behind why ClickHouse is so fast.
Table 9. ClickHouse performance settings and the defaults we read from system.settings on the 26.7 build. Session settings apply on the next query; server configuration needs a restart.
Reproduce the billion rows
The generator below produces the identical table on any ClickHouse 26.x, including chDB, so every claim here about why ClickHouse is so fast can be checked rather than believed. On two vCPU it ran at 2.01 million rows per second and finished in 498.6 s; OPTIMIZE FINAL took a further 156 s. Budget 16 GiB of disk plus one partition of headroom for the merge. Test on a copy of your own data before drawing conclusions for production, and keep a tested restore path before touching any schema that serves traffic.
Knowing why ClickHouse is so fast is the start; keeping it fast at a quadrillion rows is engineering
ChistaDATA runs 24×7×365 consultative support, ClickHouse performance engineering, platform engineering and managed services on 100% open-source ClickHouse, with a 15-minute S1 response and no vendor lock-in. If your p95 is not where this page says it should be, we find out which of the seven choices your tables are missing, with the same system tables shown here: ClickHouse consulting, ClickHouse support, ClickHouse managed services and ClickHouse migration.
Further reading
- Columnar databases vs row-based databases, measured on 20 million rows: the same mechanisms beside PostgreSQL 16, with the row-store side of every comparison above.
- How Cloudflare uses ClickHouse at quadrillion-row scale and A quadrillion rows across three clouds: scaling LogHouse, the published deployments quoted in Table 1.
- ClickBench, the independent benchmark whose methodology (cold run, then two hot runs, each reported) is the one to copy.
- ClickHouse documentation: a practical introduction to primary indexes, the mark-selection algorithm behind Figure 2.
- ClickHouse documentation: incremental materialized views, including the -State and -Merge combinator contract used in design choice 7.
- ChistaDATA University: instructor-led ClickHouse training, including MergeTree internals and ClickHouse performance engineering modules.
ChistaDATA Inc. is not affiliated with ClickHouse, Inc. ClickHouse® is a registered trademark of ClickHouse, Inc. Third-party scale figures are quoted from the linked publications. Lab behaviour was verified on open-source ClickHouse 26.7; test every change in staging before applying it to production and maintain a tested disaster-recovery posture.
Running ClickHouse in production? ChistaDATA provides ClickHouse consulting for architecture, performance and migrations, and 24×7 ClickHouse support with a 15-minute S1 response. For day-to-day operations see ClickHouse DBA services and ClickHouse managed services.