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 caseEngine
Events (append-only)MergeTree
UpsertsReplacingMergeTree
Pre-aggregatedSummingMergeTree
Incremental MVsAggregatingMergeTree
State toggleCollapsingMergeTree
Buffered ingestBuffer
ShardedDistributed
HAReplicated*

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 .