ClickHouse is a column-oriented OLAP database built for one thing: aggregating billions of rows in sub-second time. Where Postgres and MySQL are optimized for transactional reads and writes of individual rows, ClickHouse is optimized for scanning millions of rows and computing a SUM, COUNT, or AVG over them — which is exactly the shape of most analytics and observability queries.
Why columnar storage changes everything
A row-oriented database stores each record contiguously, so reading one column means touching every column along the way. ClickHouse stores each column separately, so a query that only needs event_type and amount reads only those two columns off disk — for a wide table with 50 columns, that's a 25x reduction in I/O before any indexing even comes into play. Columnar storage also compresses far better than row storage, because values within a single column tend to be similar (timestamps clustered together, a small set of repeated event types), and ClickHouse leans hard into this with per-column codecs like Delta, DoubleDelta, and LZ4/ZSTD.
The MergeTree engine family
Almost every production ClickHouse table uses some variant of the MergeTree engine. Data is written in small parts, sorted by the table's declared ORDER BY key, and background merges consolidate those parts over time — the same LSM-tree-adjacent idea used by RocksDB, just applied at the table level. The choice of ORDER BY key is the single most consequential schema decision in ClickHouse, because it determines which queries can use the sparse primary index and which fall back to a full scan.
CREATE TABLE events
(
event_time DateTime,
account_id UInt64,
event_type LowCardinality(String),
amount Decimal(18, 2)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (account_id, event_time)
TTL event_time + INTERVAL 13 MONTH;
Materialized views for pre-aggregation
ClickHouse materialized views are not a stored query re-run on read like in Postgres — they're a trigger that runs on every insert into the source table and writes the transformed result into a target table. Paired with an AggregatingMergeTree engine, this lets you maintain rolling aggregates (hourly active users, daily revenue by segment) incrementally, so a dashboard query hits a table with thousands of pre-aggregated rows instead of re-scanning billions of raw events on every page load.
A materialized view attached after the source table already has data will not backfill automatically — it only fires on rows inserted after creation. Backfilling requires manually inserting historical data into the target table, which trips up almost everyone the first time.
When ClickHouse is — and isn't — the right tool
ClickHouse is a strong fit for event analytics, observability/log aggregation, and any workload dominated by large scans with GROUP BY. It is a poor fit as a system of record: updates and deletes are asynchronous, eventually-consistent operations (`ALTER TABLE ... UPDATE/DELETE` run as background mutations, not immediate row edits), there's no meaningful concept of transactions across tables, and point lookups by a non-indexed key are slow compared to a proper OLTP database. The common architecture is Postgres or MySQL as the system of record, with change-data-capture (via Debezium or similar) streaming into ClickHouse for the analytical side.
Joins are the thing to watch
ClickHouse can do joins, but it is not a join-optimized engine the way a traditional RDBMS is — there's no cost-based optimizer picking join order the way Postgres does. Large joins between two big tables can blow available memory unless you're deliberate about join algorithm (hash, partial_merge) and which side is smaller. The idiomatic ClickHouse pattern is to denormalize at write time where practical, so the analytics table already has the dimension data flattened in, rather than joining fact and dimension tables on every query.
Wrapping string columns with a small, repeated set of values (status, event_type, country code) in LowCardinality(String) turns them into dictionary-encoded integers under the hood, shrinking both storage and query time meaningfully for almost no cost — it's one of the highest ROI schema tweaks available.
ClickHouse earns its place when the workload is genuinely analytical — high-volume writes, large aggregations, few or no per-row updates. Get the ORDER BY key and partitioning right up front, lean on materialized views for the queries that run constantly, and keep it out of the transactional path where a real RDBMS still does the job better.
Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.