ClickHouse engines.
MergeTree
Default. Best for append-only event data.
ENGINE = MergeTree()
ORDER BY (col1, col2)
PARTITION BY toYYYYMM(date)
PRIMARY KEY col1
TTL date + INTERVAL 90 DAY
SETTINGS index_granularity = 8192
ReplacingMergeTree
Deduplicates by ORDER BY key on merge:
ENGINE = ReplacingMergeTree(version)
ORDER BY (user_id, event_id)
Use FINAL to force dedup in query:
SELECT * FROM events FINAL
Or argMax(...) to get latest per key.
SummingMergeTree
Sums numeric columns matching ORDER BY:
CREATE TABLE counters (
date Date, user_id UInt32, count UInt64
) ENGINE = SummingMergeTree()
ORDER BY (date, user_id);
INSERT INTO counters VALUES (today(), 1, 1);
INSERT INTO counters VALUES (today(), 1, 1);
-- After merge: (today, 1, 2)
Pre-aggregation on write.
AggregatingMergeTree
Holds AggregateFunction state. Used with materialized views for incremental aggregations.
CREATE TABLE counts (
day Date,
state AggregateFunction(count, UInt32)
) ENGINE = AggregatingMergeTree()
ORDER BY day;
CREATE MATERIALIZED VIEW counts_mv TO counts AS
SELECT toDate(ts) AS day, countState() AS state
FROM events GROUP BY day;
-- Query
SELECT day, countMerge(state) FROM counts GROUP BY day;
CollapsingMergeTree
Toggles rows on/off via sign column. For modeling state changes.
CREATE TABLE user_state (
user_id UInt32, active UInt8, sign Int8
) ENGINE = CollapsingMergeTree(sign)
ORDER BY user_id;
INSERT INTO user_state VALUES (1, 1, 1); -- active
INSERT INTO user_state VALUES (1, 1, -1); -- delete previous
INSERT INTO user_state VALUES (1, 0, 1); -- now inactive
Use SELECT count() * sign pattern in queries.
VersionedCollapsingMergeTree
Similar with version column.
ReplicatedMergeTree
Replicated via ZooKeeper / ClickHouse Keeper:
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')
ORDER BY ...
Each replica has the full data.
Distributed
CREATE TABLE events_dist AS events
ENGINE = Distributed(cluster_name, db, events, rand())
Queries fan out to shards.
Buffer
Buffers inserts to flush in batches:
CREATE TABLE events_buffer AS events ENGINE = Buffer(db, events, 16, 10, 100, 10000, 1000000, 10000000, 100000000)
App inserts into buffer; CH flushes to real table.
File / URL
CREATE TABLE local_csv (...) ENGINE = File(CSV, '/path/data.csv');
Kafka
CREATE TABLE kafka_events (...)
ENGINE = Kafka()
SETTINGS kafka_broker_list = 'kafka:9092',
kafka_topic_list = 'events',
kafka_group_name = 'ch_consumer',
kafka_format = 'JSONEachRow';
CREATE MATERIALIZED VIEW events_consumer TO events AS
SELECT * FROM kafka_events;
Stream Kafka → ClickHouse.
S3
CREATE TABLE s3_data
ENGINE = S3('https://bucket.s3.../data.parquet', 'Parquet');
Or s3() table function:
SELECT * FROM s3('https://...', 'Parquet') LIMIT 10;
Choice guide
| Use case | Engine |
|---|---|
| Events (append-only) | MergeTree |
| Upserts | ReplacingMergeTree |
| Pre-aggregated | SummingMergeTree |
| Incremental MVs | AggregatingMergeTree |
| State toggle | CollapsingMergeTree |
| Buffered ingest | Buffer |
| Sharded | Distributed |
| HA | Replicated* |
Common mistakes
- ReplacingMergeTree without
FINAL→ see duplicates. - SummingMergeTree merging non-additive cols.
- Too many partitions → fragmented table.
- No TTL → infinite growth.
Read this next
If you want my engine selection guide, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .