Skip to content
Back to skills

Clickhouse

ASecurity

ClickHouse technology expert covering ALL versions. Deep expertise in columnar storage, MergeTree engine family, materialized views, distributed queries, and analytical query optimization. WHEN: \"ClickHouse\", \"clickhouse-client\", \"clickhouse-server\", \"MergeTree\", \"ReplacingMergeTree\", \"AggregatingMergeTree\", \"CollapsingMergeTree\", \"materialized view\", \"distributed table\", \"ClickHouse Cloud\", \"clickhouse-local\", \"ClickHouse Keeper\".

  • 4 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 24, 2026
devopsgosqlnodeexpressawstestingdatabasesecurityperformance

Works with

  • cli

Security analysis

A100/100

Pro scans all 8 files and shows the line behind each finding

Scanned September 24, 2026

npx -y skills add chrishuffman5/domain-expert --skill clickhouse --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Clickhouse?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Clickhouse
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/chrishuffman5-clickhouse/badge)](https://www.skillsdirectory.com/skills/chrishuffman5-clickhouse)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: clickhouse
description: "ClickHouse technology expert covering ALL versions. Deep expertise in columnar storage, MergeTree engine family, materialized views, distributed queries, and analytical query optimization. WHEN: \"ClickHouse\", \"clickhouse-client\", \"clickhouse-server\", \"MergeTree\", \"ReplacingMergeTree\", \"AggregatingMergeTree\", \"CollapsingMergeTree\", \"materialized view\", \"distributed table\", \"ClickHouse Cloud\", \"clickhouse-local\", \"ClickHouse Keeper\"."
license: MIT
---

# ClickHouse

This skill covers ClickHouse across all supported versions (24.8 LTS through 25.12 LTS). It covers ClickHouse internals -- columnar storage, the MergeTree engine family, distributed query execution, materialized views, projections, dictionaries, replication via ClickHouse Keeper, and analytical query optimization. For version-specific detail, see the matching file under `references/versions/`.

## When to Use This Skill vs. Version-Specific Guidance

**Use this agent when the question spans versions or is version-agnostic:**
- "How does MergeTree storage work in ClickHouse?"
- "Design a schema for a high-volume event analytics workload"
- "Tune a ClickHouse cluster for maximum query throughput"
- "Set up ReplicatedMergeTree with ClickHouse Keeper"
- "Compare AggregatingMergeTree vs. materialized views for pre-aggregation"
- "Troubleshoot 'too many parts' errors"
- "Optimize a slow GROUP BY query on a billion-row table"

**See the matching version reference when the question is version-specific:**
- "ClickHouse 25.12 LTS new features" --> `references/versions/25.12-lts.md`
- "ClickHouse 25.3 LTS lightweight deletes improvements" --> `references/versions/25.3-lts.md`
- "ClickHouse 24.8 LTS SharedMergeTree for Cloud" --> `references/versions/24.8-lts.md`

## How to Approach Tasks

When you receive a request:

1. **Classify** the request:
   - **Architecture/internals** -- Load `references/architecture.md`
   - **Performance diagnostics** -- Load `references/diagnostics.md`
   - **Configuration/operations** -- Load `references/best-practices.md`
   - **Version-specific feature** -- See the matching `references/versions/<v>.md` file
   - **Comparison with other databases** -- see the `overview` skill

2. **Determine version** -- Ask if unclear. Behavior differs across versions (e.g., lightweight deletes matured in 24.x+, SharedMergeTree only in ClickHouse Cloud, Variant type in 25.x+).

3. **Analyze** -- Apply ClickHouse-specific reasoning. Reference columnar storage characteristics, MergeTree merge mechanics, partition pruning, primary key sparse indexing, and distributed query execution as relevant.

4. **Recommend** -- Provide actionable guidance with specific server settings, SQL DDL/DML, clickhouse-client commands, or config.xml/users.xml parameters.

5. **Verify** -- Suggest validation steps (EXPLAIN PIPELINE, system.query_log, system.parts, system.merges, system.metrics).

## Core Expertise

### Columnar Storage Model

ClickHouse stores data in a column-oriented format. Each column is stored independently in separate files, enabling:

- **Compression efficiency** -- Similar values in a column compress extremely well (10:1 to 100:1 typical for analytical data). ClickHouse uses LZ4 by default with optional ZSTD, Delta, DoubleDelta, Gorilla, T64, and FPC codecs.
- **Vectorized execution** -- Queries process data in batches of column vectors (default 8192 rows per block), exploiting CPU SIMD instructions.
- **I/O minimization** -- Only columns referenced in the query are read from disk. A query touching 5 of 200 columns reads ~2.5% of the data.
- **Cache-friendly access** -- Sequential column data fits in CPU cache lines efficiently.

**Column encoding best practices:**
```sql
CREATE TABLE events (
    event_time    DateTime64(3) CODEC(DoubleDelta, LZ4),   -- timestamps: DoubleDelta
    user_id       UInt64        CODEC(Delta, LZ4),          -- monotonic IDs: Delta
    event_type    LowCardinality(String),                    -- low-cardinality strings
    url           String        CODEC(ZSTD(3)),              -- high-entropy strings: ZSTD
    response_code UInt16        CODEC(T64, LZ4),             -- small integers: T64
    latitude      Float64       CODEC(Gorilla, LZ4),         -- IEEE 754 floats: Gorilla
    payload       String        CODEC(ZSTD(1))               -- large text/JSON: ZSTD
) ENGINE = MergeTree()
ORDER BY (event_type, user_id, event_time);
```

### MergeTree Engine Family

MergeTree is the foundation of ClickHouse's storage. All production tables should use a MergeTree variant.

**How MergeTree works:**
1. Data is inserted in **parts** (immutable sorted chunks, typically millions of rows each)
2. Each part contains column files, a primary index (sparse), skip indexes, partition metadata, and checksums
3. Background **merges** combine smaller parts into larger parts, maintaining sort order
4. The **primary key** defines the sort order within parts and powers the sparse index (one index entry per `index_granularity` rows, default 8192)
5. **Partitioning** physically separates data (e.g., by month) to enable partition-level operations (DROP PARTITION, DETACH/ATTACH)

**MergeTree variants:**

| Engine | Purpose | Key Behavior |
|---|---|---|
| `MergeTree` | General-purpose analytical storage | Insert-only; parts are merged maintaining sort order |
| `ReplacingMergeTree` | Deduplication by sorting key | Keeps the latest version (by `ver` column) during merges; not guaranteed without FINAL |
| `AggregatingMergeTree` | Pre-aggregated rollups | Merges rows with same sorting key using aggregate function states |
| `SummingMergeTree` | Sum-based rollups | Sums numeric columns for rows with same sorting key during merges |
| `CollapsingMergeTree` | Mutable state via sign column | Uses Sign column (+1/-1) to cancel old rows and insert new ones |
| `VersionedCollapsingMergeTree` | Collapsing with out-of-order inserts | Adds Version column to handle out-of-order inserts correctly |
| `GraphiteMergeTree` | Graphite metric storage | Applies Graphite retention and aggregation rules during merges |

**Replicated variants:** Prefix any engine with `Replicated` (e.g., `ReplicatedMergeTree`, `ReplicatedAggregatingMergeTree`) for multi-replica support via ClickHouse Keeper.

### Primary Key and Sparse Index

ClickHouse's primary key is fundamentally different from RDBMS primary keys:

- **Not a uniqueness constraint** -- Duplicate values are allowed
- **Defines sort order** -- Data within each part is sorted by the primary key columns
- **Sparse index** -- One index entry per `index_granularity` rows (default 8192), not per row
- **Granule** -- The atomic unit of data reading; a contiguous block of `index_granularity` rows

**Primary key column ordering matters enormously:**
```sql
-- GOOD: low-cardinality first, then higher cardinality, then time
ORDER BY (tenant_id, event_type, event_time)

-- BAD: high-cardinality first destroys index effectiveness
ORDER BY (event_time, user_id, event_type)
```

**Design rules:**
1. Place the lowest-cardinality column first (the column most queries filter on)
2. Each subsequent column should increase in cardinality
3. Time columns typically go last in the ORDER BY (unless time-range is the primary access pattern)
4. The ORDER BY (sorting key) can be a superset of the PRIMARY KEY -- additional ORDER BY columns improve compression without expanding the sparse index

### Partitioning

Partitioning physically separates data within a table:

```sql
CREATE TABLE events (
    event_date Date,
    event_type String,
    user_id UInt64,
    data String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date)   -- monthly partitions
ORDER BY (event_type, user_id, event_date);
```

**Partitioning rules:**
- Use **coarse** partitions: monthly or weekly. Daily is acceptable for very high volumes. Hourly is almost always wrong.
- Target **~1,000 or fewer active partitions** per table. More than that causes excessive file descriptors and metadata overhead.
- Partition pruning eliminates entire partitions from scans when the WHERE clause matches the partition expression.
- Partition operations: `ALTER TABLE ... DROP PARTITION`, `DETACH PARTITION`, `ATTACH PARTITION`, `REPLACE PARTITION`, `FREEZE PARTITION` (for backups).
- **Do NOT over-partition.** A common mistake is partitioning by a high-cardinality column, creating millions of tiny partitions.

### Materialized Views

Materialized views in ClickHouse are insert-time triggers, not periodic refreshes:

```sql
-- Source table
CREATE TABLE raw_events (
    event_time DateTime,
    user_id UInt64,
    event_type LowCardinality(String),
    duration_ms UInt32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, user_id, event_time);

-- Destination table for the materialized view
CREATE TABLE hourly_stats (
    hour DateTime,
    event_type LowCardinality(String),
    count AggregateFunction(count),
    avg_duration AggregateFunction(avg, UInt32),
    uniq_users AggregateFunction(uniq, UInt64)
) ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (event_type, hour);

-- Materialized view: triggers on each INSERT into raw_events
CREATE MATERIALIZED VIEW hourly_stats_mv TO hourly_stats AS
SELECT
    toStartOfHour(event_time) AS hour,
    event_type,
    countState() AS count,
    avgState(duration_ms) AS avg_duration,
    uniqState(user_id) AS uniq_users
FROM raw_events
GROUP BY hour, event_type;

-- Query the materialized view target table using -Merge combinators
SELECT
    hour,
    event_type,
    countMerge(count) AS total_events,
    avgMerge(avg_duration) AS avg_duration_ms,
    uniqMerge(uniq_users) AS unique_users
FROM hourly_stats
GROUP BY hour, event_type
ORDER BY hour DESC;
```

**Key principles:**
- Materialized views process data at INSERT time, not retroactively
- Use `AggregateFunction` types with `-State` suffix in the MV and `-Merge` suffix in queries
- Materialized views can chain: MV_A --> MV_B --> MV_C
- Multiple MVs on one source table is common and efficient
- If the MV query fails, the INSERT into the source table still succeeds (data is not lost)

### Projections

Projections are inline materialized views stored within the same table:

```sql
CREATE TABLE events (
    event_time DateTime,
    user_id UInt64,
    event_type LowCardinality(String),
    country LowCardinality(String),
    duration_ms UInt32
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, user_id, event_time);

-- Add a projection for queries that filter by country
ALTER TABLE events ADD PROJECTION events_by_country (
    SELECT * ORDER BY (country, event_type, event_time)
);

-- Add an aggregating projection
ALTER TABLE events ADD PROJECTION hourly_summary (
    SELECT
        toStartOfHour(event_time) AS hour,
        event_type,
        count() AS cnt,
        avg(duration_ms) AS avg_dur
    GROUP BY hour, event_type
);

-- Materialize projections for existing data
ALTER TABLE events MATERIALIZE PROJECTION events_by_country;
ALTER TABLE events MATERIALIZE PROJECTION hourly_summary;
```

**Projections vs. Materialized Views:**
- Projections are simpler (no separate target table) but less flexible
- Projections are automatically maintained and consistent with the base table
- Materialized views allow cross-table transformations, more complex aggregations, and different engines
- The query optimizer automatically selects the best projection if applicable

### Dictionaries

Dictionaries are ClickHouse's mechanism for key-value lookups from external data sources:

```sql
CREATE DICTIONARY geo_dict (
    ip_range_start UInt32,
    ip_range_end   UInt32,
    country_code   String,
    city           String
)
PRIMARY KEY ip_range_start
SOURCE(CLICKHOUSE(TABLE 'geo_data' DB 'default'))
LAYOUT(IP_TRIE())
LIFETIME(MIN 3600 MAX 7200);

-- Usage in queries
SELECT
    event_time,
    user_ip,
    dictGet('geo_dict', 'country_code', toIPv4(user_ip)) AS country
FROM events;
```

**Dictionary layouts:**
| Layout | Use Case | Memory Model |
|---|---|---|
| `flat` | Integer keys 0..N | Array indexed by key |
| `hashed` | Integer or string keys, fits in RAM | Hash table |
| `range_hashed` | Key + date/time range lookups | Hash table with range index |
| `cache` | Large datasets, tolerates misses | LRU cache, queries source on miss |
| `complex_key_hashed` | Composite keys | Hash table with composite key |
| `ip_trie` | IP address/CIDR lookups | Trie structure |
| `direct` | Always reads from source | No cache; every lookup queries source |

### Distributed Queries and Sharding

ClickHouse supports horizontal scaling via sharding. The `Distributed` engine acts as a proxy:

```sql
-- Local table on each shard
CREATE TABLE events_local ON CLUSTER my_cluster (
    event_time DateTime,
    user_id UInt64,
    event_type String
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/events', '{replica}')
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_type, user_id, event_time);

-- Distributed table spanning all shards
CREATE TABLE events_distributed ON CLUSTER my_cluster AS events_local
ENGINE = Distributed(my_cluster, default, events_local, xxHash64(user_id));
```

**Sharding strategies:**
- **Hash-based** (`xxHash64(user_id)`): Even distribution, good for JOIN locality
- **Random** (`rand()`): Even distribution, no locality guarantees
- **Expression-based** (`toYYYYMM(event_time)`): Time-based sharding

**Distributed query execution flow:**
1. Query hits any node (the **initiator**)
2. Initiator rewrites the query and sends subqueries to each shard
3. Each shard executes locally and streams partial results back
4. Initiator merges partial results and returns the final result

**Key settings for distributed queries:**
```sql
SET distributed_product_mode = 'global';     -- for distributed JOINs
SET max_parallel_replicas = 3;               -- parallel reads from replicas
SET prefer_localhost_replica = 1;            -- prefer local shard if initiator is also a shard
```

### Replication with ClickHouse Keeper

ClickHouse Keeper (clickhouse-keeper) is the native replacement for Apache ZooKeeper, providing consensus for replicated tables:

**Replication model:**
- Each replica independently pulls data from other replicas
- The replication log is stored in ClickHouse Keeper (ZooKeeper path)
- Replicas are eventually consistent -- each replica independently fetches missing parts
- Inserts can go to any replica; the insert is logged in Keeper and other replicas fetch the part
- `ReplicatedMergeTree` is the replicated engine prefix

**ClickHouse Keeper configuration (config.xml):**
```xml
<keeper_server>
    <tcp_port>9181</tcp_port>
    <server_id>1</server_id>
    <log_storage_path>/var/lib/clickhouse/coordination/log</log_storage_path>
    <snapshot_storage_path>/var/lib/clickhouse/coordination/snapshots</snapshot_storage_path>
    <coordination_settings>
        <operation_timeout_ms>10000</operation_timeout_ms>
        <session_timeout_ms>30000</session_timeout_ms>
        <raft_logs_level>warning</raft_logs_level>
    </coordination_settings>
    <raft_configuration>
        <server>
            <id>1</id>
            <hostname>keeper1.example.com</hostname>
            <port>9234</port>
        </server>
        <server>
            <id>2</id>
            <hostname>keeper2.example.com</hostname>
            <port>9234</port>
        </server>
        <server>
            <id>3</id>
            <hostname>keeper3.example.com</hostname>
            <port>9234</port>
        </server>
    </raft_configuration>
</keeper_server>
```

**Replication monitoring:**
```sql
SELECT
    database, table, replica_name, is_leader,
    absolute_delay, queue_size, inserts_in_queue, merges_in_queue
FROM system.replicas
WHERE is_session_expired = 0
ORDER BY absolute_delay DESC;
```

### Data Types

ClickHouse provides rich type support optimized for analytics:

**Numeric types:** `UInt8`, `UInt16`, `UInt32`, `UInt64`, `UInt128`, `UInt256`, `Int8`..`Int256`, `Float32`, `Float64`, `Decimal32/64/128/256`

**String types:** `String` (arbitrary bytes), `FixedString(N)`, `LowCardinality(String)` (dictionary-encoded, major performance gain for <10K distinct values)

**Date/time types:** `Date`, `Date32`, `DateTime`, `DateTime64(precision, timezone)`

**Compound types:** `Array(T)`, `Tuple(T1, T2, ...)`, `Map(K, V)`, `Nested(col1 T1, col2 T2, ...)`

**Special types:** `Nullable(T)` (avoid when possible -- adds storage and processing overhead), `UUID`, `IPv4`, `IPv6`, `Enum8/Enum16`, `JSON` (semi-structured), `SimpleAggregateFunction`, `AggregateFunction`

**Performance tip:** Prefer `LowCardinality(String)` over plain `String` for columns with fewer than ~10,000 distinct values. It provides 5-10x compression improvement and 2-5x query speedup through dictionary encoding.

### ClickHouse SQL Extensions

See `references/operations.md#clickhouse-sql-extensions` — array functions, window functions, CTEs, FINAL modifier, SAMPLE clause, parameterized views, and FORMAT clause syntax.

### INSERT Optimization

ClickHouse is optimized for bulk inserts, not single-row writes:

**Rules of thumb:**
- **Batch size:** Insert 10,000-1,000,000 rows per INSERT statement
- **Insert frequency:** No more than ~1 insert per second per table (across all clients)
- **Async inserts:** Enable `async_insert = 1` for high-frequency small inserts; the server buffers and batches them
- **Buffer tables:** `Buffer` engine absorbs high-frequency inserts and flushes to the target MergeTree table periodically

```sql
-- Async insert settings
SET async_insert = 1;
SET wait_for_async_insert = 0;           -- don't wait for flush (fire-and-forget)
SET async_insert_max_data_size = 10485760;  -- flush buffer at 10MB
SET async_insert_busy_timeout_ms = 1000;    -- flush every 1 second

-- Bulk insert from file
clickhouse-client --query "INSERT INTO events FORMAT CSVWithNames" < events.csv
clickhouse-client --query "INSERT INTO events FORMAT Parquet" < events.parquet

-- Insert from S3
INSERT INTO events
SELECT * FROM s3('https://bucket.s3.amazonaws.com/data/*.parquet', 'Parquet');

-- Insert from another table
INSERT INTO events_archive
SELECT * FROM events WHERE event_date < today() - 90;
```

### Backup and Restore

See `references/operations.md#backup-and-restore` — BACKUP/RESTORE SQL commands, S3 backup targets, incremental backups, and alternatives like clickhouse-backup and FREEZE PARTITION.

## Troubleshooting Playbooks

### "Too Many Parts" Error

**Symptom:** Inserts fail with "Too many parts (N). Merges are processing significantly slower than inserts."

**Root cause:** Each INSERT creates a new part. If inserts arrive faster than merges can consolidate, part count grows unboundedly.

**Diagnostic:**
```sql
SELECT database, table, count() AS part_count, sum(rows) AS total_rows
FROM system.parts
WHERE active
GROUP BY database, table
ORDER BY part_count DESC;
```

**Resolution:**
1. **Reduce insert frequency** -- Batch inserts to at least 10K-100K rows per INSERT
2. **Enable async inserts** -- `SET async_insert = 1` to let the server batch
3. **Increase merge throughput** -- `background_pool_size` (default 16), `background_merges_mutations_concurrency_ratio`
4. **Check for stuck merges** -- `SELECT * FROM system.merges`
5. **Temporary relief** -- `OPTIMIZE TABLE events FINAL` (blocks until merges complete, resource-intensive)

### Slow Queries

**Diagnostic sequence:**
```sql
-- 1. Check query execution plan
EXPLAIN PIPELINE SELECT ... ;
EXPLAIN PLAN actions=1 SELECT ... ;

-- 2. Find slow queries in the log
SELECT query, query_duration_ms, read_rows, read_bytes, memory_usage
FROM system.query_log
WHERE type = 'QueryFinish'
  AND query_duration_ms > 5000
ORDER BY query_duration_ms DESC
LIMIT 20;

-- 3. Check if primary key is being used (look for granules scanned vs total)
SELECT query, ProfileEvents['SelectedMarks'] AS marks_selected,
       ProfileEvents['SelectedRows'] AS rows_selected
FROM system.query_log
WHERE query_id = 'your-query-id';
```

**Common causes and fixes:**
- **Full table scan** -- Query does not match the primary key prefix. Reorder ORDER BY or add a projection.
- **Excessive memory** -- Large GROUP BY or JOIN. Use `max_bytes_before_external_group_by` or `max_bytes_before_external_sort`.
- **Distributed query amplification** -- Suboptimal distributed JOIN. Use `GLOBAL IN` / `GLOBAL JOIN` or co-locate data on the same sharding key.
- **Missing codec** -- Large string columns without ZSTD. Add compression codecs.
- **Nullable overhead** -- Replace `Nullable(T)` with default values where possible.

### Replication Lag

**Diagnostic:**
```sql
SELECT database, table, replica_name, is_leader,
       absolute_delay, queue_size,
       inserts_in_queue, merges_in_queue,
       last_queue_update
FROM system.replicas
ORDER BY absolute_delay DESC;
```

**Resolution:**
1. Check Keeper health: `SELECT * FROM system.zookeeper WHERE path = '/clickhouse'`
2. Check network between replicas
3. Increase `background_schedule_pool_size` for replication tasks
4. Check for large parts that are slow to transfer: `SELECT * FROM system.replication_queue WHERE is_currently_executing = 1`

### OOM (Out of Memory)

**Prevention settings:**
```sql
SET max_memory_usage = 10000000000;                    -- 10GB per query
SET max_memory_usage_for_user = 20000000000;           -- 20GB per user
SET max_bytes_before_external_group_by = 5000000000;   -- spill GROUP BY to disk at 5GB
SET max_bytes_before_external_sort = 5000000000;       -- spill ORDER BY to disk at 5GB
SET max_rows_to_read = 1000000000;                     -- safety limit
```

**Diagnostic:**
```sql
SELECT query, memory_usage, peak_memory_usage
FROM system.query_log
WHERE type = 'ExceptionWhileProcessing'
  AND exception LIKE '%MEMORY_LIMIT_EXCEEDED%'
ORDER BY event_time DESC
LIMIT 10;
```

## Version Matrix

| Version | Type | Release | Status (April 2026) |
|---|---|---|---|
| 25.12 LTS | Long-Term Support | Dec 2025 | Current LTS -- actively maintained |
| 25.3 LTS | Long-Term Support | Mar 2025 | Supported LTS -- security and bug fixes |
| 24.8 LTS | Long-Term Support | Aug 2024 | Supported LTS -- security fixes until Aug 2026 |

**Version naming convention:** ClickHouse uses `YY.MM` naming (e.g., 24.8 = August 2024 release). LTS releases receive ~2 years of maintenance. Non-LTS releases are supported for approximately 6 months after the next release.

**Recommendation:** Use the latest LTS version for production (25.12 LTS). Use the latest stable release for development/testing to access the newest features.

Files in this skill

  • SKILL.md22.1 KB
  • references/architecture.md22 KB
  • references/best-practices.md21.6 KB
  • references/diagnostics.md38.5 KB
  • references/operations.md2.2 KB
  • references/versions/24.8-lts.md8.2 KB
  • references/versions/25.12-lts.md14.4 KB
  • references/versions/25.3-lts.md11 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…