ClickHouse Skill
When to use
- Designing ClickHouse schemas for event streams, log pipelines, or analytical dashboards
- Choosing the right table engine (MergeTree family, ReplacingMergeTree, AggregatingMergeTree, CollapsingMergeTree, Distributed)
- Writing efficient OLAP queries (aggregations, window functions, array operations)
- Setting up materialized views for pre-aggregation
- Configuring replication with ClickHouse Keeper or ZooKeeper
- Ingesting data from Kafka, S3, HTTP, or other ClickHouse engines
- Diagnosing slow queries via
EXPLAIN, system.query_log, and system.parts
Workflow
- Define the query patterns first — ClickHouse schema design is query-driven. Know the top 5 queries that must be fast before choosing an ORDER BY key or partition expression.
- Choose the table engine:
MergeTree — default; immutable append-only facts.
ReplacingMergeTree — deduplicate rows by primary key (eventual; use FINAL in queries to force merge).
AggregatingMergeTree — store partial aggregation states for incremental rollups.
CollapsingMergeTree — OLAP-style deletes via sign column.
Distributed — query shard cluster; always back it with local replicated tables.
Kafka — read directly from Kafka topics (use as a source, not a storage engine).
- Design the ORDER BY (primary key) carefully — ClickHouse stores data sorted by the
ORDER BY columns. Put low-cardinality columns first (e.g. (event_type, toDate(timestamp), user_id)). The primary key determines sparse index granularity.
- Set a
PARTITION BY clause — use date-level granularity for time-series: PARTITION BY toYYYYMM(timestamp). This enables partition pruning and TTL-based data expiry.
- Use materialized views for pre-aggregation — create a target
AggregatingMergeTree table and a MATERIALIZED VIEW that feeds into it on insert. This moves computation from query time to ingest time.
- Batch inserts — never insert one row at a time. Minimum effective batch: 1 000 rows; recommended: 10 000–100 000 rows per INSERT. Use the async insert mode (
async_insert=1) for high-frequency small-batch sources.
- Tune with
EXPLAIN — run EXPLAIN indexes=1 <query> to confirm index granules are being skipped. If Granules: <large number>, revisit ORDER BY or add a skip index.
- Set TTL for automatic data expiry:
TTL timestamp + INTERVAL 90 DAY DELETE.
- Monitor via
system.query_log, system.parts, system.merges. Alert on merge queue depth and part count per table (SELECT count() FROM system.parts WHERE table = 'events' AND active).
Standards
Do
- Use
UInt32/UInt64 for IDs and counters — smaller types compress better and query faster than String UUIDs unless UUID comparison is required.
- Use
LowCardinality(String) for columns with < ~10 000 distinct values (event type, status, country code).
- Use
Nullable(T) sparingly — nullable columns disable some optimizations; prefer a sentinel value (empty string, 0) when the null state has no semantic meaning.
- Compress columns explicitly for high-cardinality strings:
CODEC(ZSTD(3)).
- Run DDL changes (ADD COLUMN, DROP COLUMN) on all replicas via the
ON CLUSTER clause in replicated setups.
- Test queries on a sampled dataset:
SELECT … FROM table SAMPLE 0.01 before running on the full table.
Do not
- Do not use
JOIN as the primary access pattern — ClickHouse is optimized for denormalized wide tables. Pre-join data at ingest or use dictionaries for small reference tables.
- Do not
UPDATE or DELETE rows frequently — mutations in ClickHouse are expensive async rewrites. Use ReplacingMergeTree or CollapsingMergeTree for mutable data.
- Do not
SELECT * in OLAP queries — ClickHouse is columnar; reading unused columns wastes I/O.
- Do not use
ORDER BY rand() for sampling — use SAMPLE clause or sipHash64(id) % N = 0.
- Do not create more than ~500 partitions per table — too many small parts degrade merge performance.
- Do not expose ClickHouse's native TCP port (9000) or HTTP port (8123) to the internet without authentication and TLS termination.
Common mistakes to avoid
| Mistake |
Consequence |
Fix |
| Wrong ORDER BY (high-cardinality first) |
Index granules not skipped; full scan on every query |
Put low-cardinality columns first in ORDER BY |
| Inserting one row per INSERT |
Extremely slow ingest; merge storm |
Buffer inserts client-side; send 10k+ rows per batch |
Using ReplacingMergeTree without FINAL |
Duplicate rows returned |
Append FINAL to SELECT, or use GROUP BY with argMax |
| Too many partitions (daily partition on 3 years of data) |
1000+ parts per table; slow queries |
Switch to monthly partition; merge old partitions with OPTIMIZE TABLE … FINAL |
Missing ON CLUSTER in DDL on replicated cluster |
Schema drift between shards |
Always suffix DDL with ON CLUSTER <cluster_name> |
| Not setting TTL |
Unbounded disk growth |
Define TTL at table creation; add TTL to existing tables with ALTER TABLE … MODIFY TTL |
Using String for UUIDs in JOINs |
Poor join performance |
Use UUID type or UInt64 hash of UUID |
Output format
Table definition pattern:
CREATE TABLE events
(
timestamp DateTime64(3) CODEC(Delta, ZSTD(1)),
event_type LowCardinality(String),
user_id UInt64,
session_id UInt64,
properties String CODEC(ZSTD(3))
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (event_type, toDate(timestamp), user_id)
TTL timestamp + INTERVAL 365 DAY DELETE
SETTINGS index_granularity = 8192;
Materialized view pattern:
CREATE TABLE events_daily_agg
(
event_type LowCardinality(String),
date Date,
count AggregateFunction(count, UInt64)
)
ENGINE = AggregatingMergeTree()
ORDER BY (event_type, date);
CREATE MATERIALIZED VIEW events_daily_mv TO events_daily_agg AS
SELECT event_type, toDate(timestamp) AS date, countState() AS count
FROM events
GROUP BY event_type, date;
Related checklists
.claude/checklists/performance.md
.claude/checklists/database.md
.claude/checklists/devops.md
Related agents
.claude/agents/engineering/backend-engineer.md
.claude/agents/engineering/data-engineer.md
.claude/agents/quality/performance-engineer.md
1---2name: clickhouse3description: Use when the project uses ClickHouse — OLAP analytics, event ingestion, time-series, log storage, table engines, materialized views, sharding/replication, slow analytic queries.4---56# ClickHouse Skill78## When to use910- Designing ClickHouse schemas for event streams, log pipelines, or analytical dashboards11- Choosing the right table engine (MergeTree family, ReplacingMergeTree, AggregatingMergeTree, CollapsingMergeTree, Distributed)12- Writing efficient OLAP queries (aggregations, window functions, array operations)13- Setting up materialized views for pre-aggregation14- Configuring replication with ClickHouse Keeper or ZooKeeper15- Ingesting data from Kafka, S3, HTTP, or other ClickHouse engines16- Diagnosing slow queries via `EXPLAIN`, `system.query_log`, and `system.parts`1718---1920## Workflow21221. **Define the query patterns first** — ClickHouse schema design is query-driven. Know the top 5 queries that must be fast before choosing an ORDER BY key or partition expression.232. **Choose the table engine**:24 - `MergeTree` — default; immutable append-only facts.25 - `ReplacingMergeTree` — deduplicate rows by primary key (eventual; use `FINAL` in queries to force merge).26 - `AggregatingMergeTree` — store partial aggregation states for incremental rollups.27 - `CollapsingMergeTree` — OLAP-style deletes via sign column.28 - `Distributed` — query shard cluster; always back it with local replicated tables.29 - `Kafka` — read directly from Kafka topics (use as a source, not a storage engine).303. **Design the ORDER BY (primary key) carefully** — ClickHouse stores data sorted by the `ORDER BY` columns. Put low-cardinality columns first (e.g. `(event_type, toDate(timestamp), user_id)`). The primary key determines sparse index granularity.314. **Set a `PARTITION BY` clause** — use date-level granularity for time-series: `PARTITION BY toYYYYMM(timestamp)`. This enables partition pruning and TTL-based data expiry.325. **Use materialized views for pre-aggregation** — create a target `AggregatingMergeTree` table and a `MATERIALIZED VIEW` that feeds into it on insert. This moves computation from query time to ingest time.336. **Batch inserts** — never insert one row at a time. Minimum effective batch: 1 000 rows; recommended: 10 000–100 000 rows per INSERT. Use the async insert mode (`async_insert=1`) for high-frequency small-batch sources.347. **Tune with `EXPLAIN`** — run `EXPLAIN indexes=1 <query>` to confirm index granules are being skipped. If `Granules: <large number>`, revisit ORDER BY or add a skip index.358. **Set TTL** for automatic data expiry: `TTL timestamp + INTERVAL 90 DAY DELETE`.369. **Monitor** via `system.query_log`, `system.parts`, `system.merges`. Alert on merge queue depth and part count per table (`SELECT count() FROM system.parts WHERE table = 'events' AND active`).3738---3940## Standards4142### Do43- Use `UInt32`/`UInt64` for IDs and counters — smaller types compress better and query faster than `String` UUIDs unless UUID comparison is required.44- Use `LowCardinality(String)` for columns with < ~10 000 distinct values (event type, status, country code).45- Use `Nullable(T)` sparingly — nullable columns disable some optimizations; prefer a sentinel value (empty string, 0) when the null state has no semantic meaning.46- Compress columns explicitly for high-cardinality strings: `CODEC(ZSTD(3))`.47- Run DDL changes (ADD COLUMN, DROP COLUMN) on all replicas via the `ON CLUSTER` clause in replicated setups.48- Test queries on a sampled dataset: `SELECT … FROM table SAMPLE 0.01` before running on the full table.4950### Do not51- Do not use `JOIN` as the primary access pattern — ClickHouse is optimized for denormalized wide tables. Pre-join data at ingest or use dictionaries for small reference tables.52- Do not `UPDATE` or `DELETE` rows frequently — mutations in ClickHouse are expensive async rewrites. Use `ReplacingMergeTree` or `CollapsingMergeTree` for mutable data.53- Do not `SELECT *` in OLAP queries — ClickHouse is columnar; reading unused columns wastes I/O.54- Do not use `ORDER BY rand()` for sampling — use `SAMPLE` clause or `sipHash64(id) % N = 0`.55- Do not create more than ~500 partitions per table — too many small parts degrade merge performance.56- Do not expose ClickHouse's native TCP port (9000) or HTTP port (8123) to the internet without authentication and TLS termination.5758---5960## Common mistakes to avoid6162| Mistake | Consequence | Fix |63|---|---|---|64| Wrong ORDER BY (high-cardinality first) | Index granules not skipped; full scan on every query | Put low-cardinality columns first in ORDER BY |65| Inserting one row per INSERT | Extremely slow ingest; merge storm | Buffer inserts client-side; send 10k+ rows per batch |66| Using `ReplacingMergeTree` without `FINAL` | Duplicate rows returned | Append `FINAL` to SELECT, or use `GROUP BY` with `argMax` |67| Too many partitions (daily partition on 3 years of data) | 1000+ parts per table; slow queries | Switch to monthly partition; merge old partitions with `OPTIMIZE TABLE … FINAL` |68| Missing `ON CLUSTER` in DDL on replicated cluster | Schema drift between shards | Always suffix DDL with `ON CLUSTER <cluster_name>` |69| Not setting TTL | Unbounded disk growth | Define TTL at table creation; add TTL to existing tables with `ALTER TABLE … MODIFY TTL` |70| Using `String` for UUIDs in JOINs | Poor join performance | Use `UUID` type or `UInt64` hash of UUID |7172---7374## Output format7576Table definition pattern:77```sql78CREATE TABLE events79(80 timestamp DateTime64(3) CODEC(Delta, ZSTD(1)),81 event_type LowCardinality(String),82 user_id UInt64,83 session_id UInt64,84 properties String CODEC(ZSTD(3))85)86ENGINE = MergeTree()87PARTITION BY toYYYYMM(timestamp)88ORDER BY (event_type, toDate(timestamp), user_id)89TTL timestamp + INTERVAL 365 DAY DELETE90SETTINGS index_granularity = 8192;91```9293Materialized view pattern:94```sql95CREATE TABLE events_daily_agg96(97 event_type LowCardinality(String),98 date Date,99 count AggregateFunction(count, UInt64)100)101ENGINE = AggregatingMergeTree()102ORDER BY (event_type, date);103104CREATE MATERIALIZED VIEW events_daily_mv TO events_daily_agg AS105SELECT event_type, toDate(timestamp) AS date, countState() AS count106FROM events107GROUP BY event_type, date;108```109110---111112## Related checklists113- `.claude/checklists/performance.md`114- `.claude/checklists/database.md`115- `.claude/checklists/devops.md`116117## Related agents118- `.claude/agents/engineering/backend-engineer.md`119- `.claude/agents/engineering/data-engineer.md`120- `.claude/agents/quality/performance-engineer.md`