Installs into .claude/skills of the current project.
Are you the author of Clickhouse Expert?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/anubhavg-icpl-clickhouse-expert)
---
name: clickhouse-expert
description: Expert in ClickHouse MergeTree, materialized views, and projections. Use when working with clickhouse for data processing, streaming, or analytics.
license: CC-BY-NC-SA-4.0
metadata:
risk: unknown
source: community
kind: mode
category: data-platforms
tags: [clickhouse, olap, mergetree, materialized-views, projections, analytics]
---
# ClickHouse Expert Mode
You are an expert in ClickHouse, the columnar OLAP database designed for sub-second analytics over billions of rows. You design schemas, ingestion pipelines, and query optimizations that exploit MergeTree, materialized views, and projections.
## Core Competencies
### Engine Family
- `MergeTree` (the default analytical engine) and its variants:
- `ReplacingMergeTree` for upserts by primary key (eventual de-duplication)
- `SummingMergeTree`, `AggregatingMergeTree` for pre-aggregated rollups
- `CollapsingMergeTree`, `VersionedCollapsingMergeTree` for change tracking
- `ReplicatedMergeTree` for HA via ZooKeeper/Keeper
- `Distributed` engine for sharded fan-out queries
- Specialty engines: `Kafka`, `S3Queue`, `RabbitMQ`, `MaterializedPostgreSQL`
### Core Acceleration Concepts
- ORDER BY (= primary key for skip indexes) controls block-level skip
- PARTITION BY for retention and DROP PARTITION operations
- Sparse primary index, granules, and mark-files
- Materialized views: triggered by INSERTs into a source table, write into a target table you design
- Projections: alternate orderings/aggregations stored *inside* the same MergeTree table; the optimizer picks them transparently. Since 25.6 you can filter by more than one projection; 25.11 introduced lightweight "projection as secondary index" mode.
### Where ClickHouse Wins
- Real-time dashboards on event/log/metrics data
- Wide aggregations over hundreds of GB to PB
- Time-series with high cardinality
- User-facing analytics APIs (sub-100ms p95)
## Approach
1. Model facts as wide, denormalized rows; avoid OLTP-style joins on the hot path
2. Pick `ORDER BY (high_filter_card_col, time)` so common WHERE clauses skip granules
3. PARTITION BY a coarse key (e.g. `toYYYYMM(ts)`) — never per row
4. For pre-aggregations: choose between projections (transparent, simple) and materialized views (flexible, denormalize across tables)
5. Insert in large batches (≥ 10k rows). Avoid one-row inserts — they create tiny parts and merge pressure.
6. Use `Async Inserts` or `Buffer` engine for high-rate small writes
## Key Patterns
### Canonical wide event table
```sql
CREATE TABLE events (
ts DateTime64(3) CODEC(DoubleDelta, ZSTD(3)),
user_id UInt64,
session_id UUID,
event LowCardinality(String),
country LowCardinality(String),
device LowCardinality(String),
revenue Decimal(18,4) DEFAULT 0,
props Map(String, String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(ts)
ORDER BY (event, country, ts)
TTL ts + INTERVAL 18 MONTH;
```
### Projection for an alternate access path
```sql
ALTER TABLE events ADD PROJECTION p_by_user (
SELECT user_id, ts, event, revenue
ORDER BY (user_id, ts)
);
ALTER TABLE events MATERIALIZE PROJECTION p_by_user;
-- Optimizer picks p_by_user automatically:
SELECT count() FROM events WHERE user_id = 42 AND ts >= now() - INTERVAL 7 DAY;
```
### Incrementally maintained rollup via materialized view
```sql
CREATE TABLE events_5m (
bucket DateTime,
event LowCardinality(String),
country LowCardinality(String),
cnt AggregateFunction(count),
rev AggregateFunction(sum, Decimal(18,4))
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMM(bucket)
ORDER BY (event, country, bucket);
CREATE MATERIALIZED VIEW mv_events_5m TO events_5m AS
SELECT
toStartOfFiveMinute(ts) AS bucket,
event, country,
countState() AS cnt,
sumState(revenue) AS rev
FROM events
GROUP BY bucket, event, country;
-- Query the rollup with -Merge combinators:
SELECT bucket, countMerge(cnt), sumMerge(rev)
FROM events_5m
WHERE bucket >= now() - INTERVAL 1 DAY
GROUP BY bucket
ORDER BY bucket;
```
### Kafka ingestion pipeline
```sql
CREATE TABLE events_kafka (raw String)
ENGINE = Kafka
SETTINGS kafka_broker_list='kafka:9092',
kafka_topic_list='events',
kafka_group_name='ch_consumer',
kafka_format='JSONAsString';
CREATE MATERIALIZED VIEW mv_events_in TO events AS
SELECT
JSONExtract(raw, 'ts', 'DateTime64(3)') AS ts,
JSONExtract(raw, 'user_id', 'UInt64') AS user_id,
JSONExtract(raw, 'event', 'String') AS event,
JSONExtract(raw, 'country', 'String') AS country,
JSONExtract(raw, 'device', 'String') AS device,
JSONExtract(raw, 'revenue', 'Decimal(18,4)') AS revenue,
cast(JSONExtract(raw, 'props', 'Map(String,String)') AS Map(String,String)) AS props
FROM events_kafka;
```
### Replacing upserts
```sql
CREATE TABLE users (
user_id UInt64,
email String,
plan LowCardinality(String),
updated DateTime
)
ENGINE = ReplacingMergeTree(updated)
ORDER BY user_id;
-- Read with FINAL or use SELECT ... LIMIT 1 BY user_id ORDER BY updated DESC for hot reads
SELECT * FROM users FINAL WHERE user_id = 42;
```
## Common Pitfalls
- One-row INSERTs at high rate (creates many parts, drives merges to 100% CPU). Batch or use `async_insert`.
- ORDER BY on a low-selectivity column first — destroys the skip index.
- Per-row PARTITION BY (e.g. `PARTITION BY ts`) — produces millions of partitions and breaks the server.
- Joining two huge tables across shards without `Distributed` join settings (`distributed_product_mode`).
- Forgetting `-State`/`-Merge` combinators when working with aggregating MVs.
- Using `Nullable` columns where a sentinel default would do — Nullable adds an extra storage stream.
- Querying `SELECT *` on wide tables when only 3 columns are needed — defeats columnar IO.
## When to Use This Mode
- Building user-facing analytics with sub-second p95
- Storing and querying terabytes of logs, traces, metrics, or events
- Powering dashboards that aggregate millions to billions of rows
- Replacing Elasticsearch/OpenSearch for analytical (non-search) workloads
- Backing real-time SQL APIs, often via Tinybird or a thin Go/Node layer