clickhouse
Skill1 starsChanged 3 months agoMITView on GitHub
Load for ClickHouse (clickhouse-client, clickhouse-local, chdb, ClickHouse Cloud), MergeTree-family engines (Replacing/Aggregating/Summing), or OLAP, time-series aggregation, and data warehouse queries targeting ClickHouse.
7 other projects use a version of this.
Adopted by
Most recently taken up by
- MMACABRE/SKILLStheir own version, first seen Nov 8, 2026
- speakeasy-api/gramtheir own version, first seen Nov 6, 2026
- G1Joshi/Agent-Skillstheir own version, first seen Oct 13, 2026
- nc9/skillstheir own version, first seen Sep 19, 2026
- muxammadmamajonov/dot-claudetheir own version, first seen Jul 6, 2026
- cline/skillstheir own version, first seen Jun 18, 2026
Other versions7 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 7
- speakeasy-api/gram★ 277same name
- cline/plugins★ 36same name
- cline/skills★ 35same name
- G1Joshi/Agent-Skills★ 15same name
- nc9/skills★ 4same name
- MMACABRE/SKILLS★ 1same name
- muxammadmamajonov/dot-claude★ 0same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository18 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- distributed-systemsSkill
- flink-javaSkill
- flink-pythonSkill
- flink-sqlSkill
- gitSkill
- grokSkill
- java-prod-skillSkill
- javaSkill
- mermaid-skillSkill
- openapi_docSkill
- postgresSkill
- python-numericsSkill
- pythonSkill
- pytorchSkill
- reactSkill
- rustSkill
- sqlSkill
The file
We split this skill into the rules it gives an agent
5 rules. 0 are common, 3 are distinctive, and 2 are very specific to this project.
0
Common
Most of the 7 edited versions we read keep these.
3
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 84
Nullable is expensive: Every Nullable(T) column stores a separate bitmask file.
- Distinctive: line 312
Always batch: 10K+ rows per insert.
- Distinctive: line 328
When clients can't batch, let the server buffer:
- Very specific: line 418
Verify granule pruning with explain indexes = 1 select ....
- Distinctive: line 449
Keep the work plan out of comments: no phases, plan steps, ticket numbers, or words like "for now", "temporary", or "later".
“Common” is measured: kept by at least 6 in 10 of the 7 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Follows universal SQL style from sql-skill (lowercase, snake_case, trailing commas, CTEs). Exception: ClickHouse built-in functions are case-sensitive camelCase (
countIf,toStartOfHour,arrayJoin) — never lowercase them. Keywords and identifiers stay lowercase/snake_case.
Philosophy
ClickHouse is a columnar OLAP engine. Different rules than OLTP:
- Denormalization is correct — pre-join at insert time, avoid runtime JOINs
- Batch everything — never single-row inserts (10K+ rows per batch)
- Select only needed columns — each column is a separate file,
select *reads everything - Approximate is often good enough —
uniq()overuniqExact(), quantile over exact percentiles - Sort key is everything — data is physically ordered by
order by, choose it for your query patterns - Partitioning is for lifecycle — partition by month for TTL/deletion, not for query performance
- Immutable data model — design for append-only, use ReplacingMergeTree for mutations
Table Engines
MergeTree (Default)
create table events (
event_id uuid default generateUUIDv4(),
event_type LowCardinality(String),
user_id UInt64,
properties JSON, -- 25.3+; keep String when the payload is opaque (never field-queried)
created_at DateTime64(3) default now64(3)
)
engine = MergeTree()
partition by toYYYYMM(created_at)
order by (event_type, user_id, created_at)
ttl created_at + interval 1 year;Engine Selection
| Engine | Use for |
MergeTree | Default. Append-only event data, logs, metrics. |
ReplacingMergeTree(version) | Mutable rows. Keeps latest by sort key. Use final to deduplicate at query time. Optional second arg is_deleted (UInt8) handles upsert+delete streams. |
AggregatingMergeTree | Pre-aggregated data. Combine with materialized views for real-time rollups. |
CollapsingMergeTree(sign) | Legacy row-level deletes/updates via +1/-1 sign column — prefer ReplacingMergeTree(version, is_deleted). |
SummingMergeTree(columns) | Auto-sum numeric columns on merge. Counters, running totals. |
ReplacingMergeTree (Mutable State)
create table users (
user_id UInt64,
email String,
name String,
updated_at DateTime64(3)
)
engine = ReplacingMergeTree(updated_at)
order by user_id;
-- read latest version (deduplicates at query time)
select * from users final where user_id = 123;
-- admin-only full rewrite; never routine — rely on final at query time or background merges
optimize table users final;Data Types
| Use | Type | Not |
| Strings (low cardinality) | LowCardinality(String) | String — for status, country, category, etc. |
| Strings (high cardinality) | String | FixedString(N) unless truly fixed-width |
| Timestamps | DateTime | DateTime64(3) — only when sub-second precision is genuinely needed (event ordering, latency measurement). Date for date-only fields. |
| Integers | Smallest sufficient type, unsigned when non-negative (UInt8/UInt16/UInt32…) | Defaulting everything to UInt64/Int64 |
| IDs | UInt64 or UUID | String for numeric IDs |
| Semi-structured (field-queried) | JSON (25.3+) | String — only for opaque payloads stored and returned whole |
| Booleans | Bool | UInt8 (0/1) — only when the column doubles as an arithmetic flag |
| Nullable | Avoid | Nullable(T) — stored in separate file, hurts performance |
Nullable is expensive. Every Nullable(T) column stores a separate bitmask file. Use defaults:
-- good: empty string instead of null
email String default '',
-- bad: separate null bitmap, slower scans
email Nullable(String)Sort Key Design
The order by clause defines physical data order AND primary index. The single most important design decision.
-- query: "events for user X in time range"
order by (user_id, created_at)
-- query: "events by type in time range"
order by (event_type, created_at)
-- query: "events in time range across all users"
order by (created_at)Rules:
- Low-cardinality columns first — better compression
- Columns from your
whereclause, in order of selectivity - Time column usually last — for range queries within the filtered set
Materialized Views (Real-Time Aggregation)
ClickHouse materialized views are insert triggers — they transform data as it arrives.
-- source: raw events
create table raw_events (
event_type LowCardinality(String),
user_id UInt64,
amount Decimal(18, 2),
created_at DateTime64(3)
) engine = MergeTree()
order by (event_type, created_at);
-- target: pre-aggregated hourly stats
create table hourly_stats (
hour DateTime,
event_type LowCardinality(String),
event_count AggregateFunction(count),
total_amount AggregateFunction(sum, Decimal(18, 2)),
unique_users AggregateFunction(uniq, UInt64)
) engine = AggregatingMergeTree()
order by (event_type, hour);
-- materialized view: transforms on insert
create materialized view hourly_stats_mv to hourly_stats as
select
toStartOfHour(created_at) as hour,
event_type,
countState() as event_count,
sumState(amount) as total_amount,
uniqState(user_id) as unique_users
from raw_events
group by hour, event_type;
-- query: merge partial aggregates
select
hour,
event_type,
countMerge(event_count) as events,
sumMerge(total_amount) as total,
uniqMerge(unique_users) as users
from hourly_stats
group by hour, event_type
order by hour;Approximate Functions
Faster than exact. Often good enough for analytics.
-- approximate distinct count (adaptive sampling; recommended over uniqExact/uniqCombined in almost all cases)
select uniq(user_id) as approx_users from events
-- exact count (slower)
select uniqExact(user_id) as exact_users from events
-- quantiles (reservoir sampling, 8192 reservoir — non-deterministic)
-- use quantileTDigest for accuracy on large sets, quantileExact when determinism matters
select
quantile(0.5)(response_time) as p50,
quantile(0.95)(response_time) as p95,
quantile(0.99)(response_time) as p99
from requests
-- multiple quantiles in one pass over one shared state
select quantiles(0.5, 0.95, 0.99)(response_time) as percentiles
from requestsConditional Aggregation (-If Combinators)
select
toStartOfDay(created_at) as day,
count() as total,
countIf(status = 'completed') as completed,
sumIf(amount, status = 'completed') as completed_revenue,
uniqIf(user_id, status = 'completed') as paying_users
from orders
group by day
order by dayAny aggregate function supports the -If suffix: countIf, sumIf, avgIf, uniqIf, quantileIf, etc.
Array Functions
First-class array support — powerful for analytics.
-- aggregate into array
select user_id, groupArray(product_id) as products
from purchases group by user_id
-- expand array to rows
select user_id, arrayJoin(tags) as tag from users
-- transform arrays
select arrayMap(x -> x * 2, [1, 2, 3]) as doubled
select arrayFilter(x -> x > 2, [1, 2, 3, 4, 5]) as filtered
-- set operations
select arrayIntersect([1,2,3], [2,3,4]) as common
-- has / hasAll / hasAny
select * from articles where hasAny(tags, ['rust', 'performance'])argMax / argMin
Get a column's value at the row where another column is max/min. Elegant alternative to subqueries.
-- latest status per user (no subquery, no window function)
select
user_id,
argMax(status, updated_at) as latest_status,
argMax(email, updated_at) as latest_email,
max(updated_at) as last_updated
from user_events
group by user_id
-- cheapest product per category
select
category,
argMin(product_name, price) as cheapest_product,
min(price) as lowest_price
from products
group by categoryJOINs in ClickHouse
JOINs work but are expensive. Design to minimize them.
-- right table loaded into memory by default
-- put the SMALLER table on the RIGHT side
select e.event_type, u.name, count()
from events as e
inner join users as u on u.id = e.user_id
group by e.event_type, u.namePrefer (in order):
- Denormalize at insert time — best performance
- Dictionaries for dimension lookups — loaded in memory
- Subqueries with
infor filtering —where user_id in (select id from users where ...) - JOINs — last resort for large-large joins
Dictionaries
Fast key-value lookups for dimension tables. Loaded into memory.
create dictionary user_dict (
id UInt64,
name String,
email String
)
primary key id
source(clickhouse(table 'users'))
layout(hashed())
lifetime(min 300 max 600);
-- avoids JOIN entirely
select
event_type,
dictGet('user_dict', 'name', user_id) as user_name,
count() as events
from events
group by event_type, user_nameInsert Patterns
Always batch: 10K+ rows per insert. Never single-row inserts in a loop.
-- insert from select (ETL pattern)
insert into daily_stats
select
toDate(created_at) as day,
event_type,
count() as event_count
from events
where created_at >= today() - 1
group by day, event_type;Async Inserts (High-Frequency Small Inserts)
When clients can't batch, let the server buffer:
insert into events settings async_insert = 1, wait_for_async_insert = 1
values ('click', 1, now());wait_for_async_insert = 0 (fire-and-forget) risks silent data loss — errors only surface at flush and nothing is acknowledged durable. Buffer tables are the legacy alternative, recommended only in rare cases (no dedup on replicated targets, data loss on crash).
TTL (Data Lifecycle)
-- auto-delete old data
create table logs (
message String,
created_at DateTime
) engine = MergeTree()
order by created_at
ttl created_at + interval 90 day;
-- tiered storage before deletion
ttl created_at + interval 30 day to volume 'cold',
created_at + interval 365 day delete;
-- column-level TTL (null out PII after retention period)
alter table users modify column email String
ttl created_at + interval 30 day;Deletes & Mutations
-- occasional row deletes (GDPR, corrections): lightweight delete, *MergeTree family
delete from events where user_id = 123;
-- heavy last-resort mutation: rewrites affected parts
alter table events update status = 'void' where order_id = 42;
-- bulk lifecycle: drop whole partitions or use TTL — never bulk lightweight deletes
alter table events drop partition '202605';Lightweight-deleted rows are only masked (hidden _row_exists column) until a merge removes them. On tables with projections it throws by default — set lightweight_mutation_projection_mode = 'drop' or 'rebuild'.
Projections
Alternative sort orders stored alongside the table. Query optimizer picks the best one automatically.
create table events (
event_type LowCardinality(String),
user_id UInt64,
created_at DateTime64(3)
) engine = MergeTree()
order by (event_type, created_at);
-- projection: same data, different sort order
alter table events add projection events_by_user (
select * order by (user_id, created_at)
);
alter table events materialize projection events_by_user;
-- queries filtering by user_id automatically use the projection
select * from events where user_id = 123;Data Skipping Indexes
Lightweight secondary access paths for filters off the sort key — try before a projection (which copies the data).
alter table events add index idx_trace trace_id type bloom_filter granularity 4;
alter table events materialize index idx_trace; -- required for pre-existing data| Type | Use for |
bloom_filter | Point lookups on high-cardinality non-sort-key columns (trace_id, url) |
minmax | Numerics loosely correlated with sort order (timestamps, counters) |
text (GA 26.2+; tokenbf_v1 on older versions) | Token search in log/text columns |
Verify granule pruning with explain indexes = 1 select ....
explain pipeline
explain pipeline
select event_type, count()
from events
where created_at > today() - 7
group by event_type
order by count() desc;Per-Query Performance Settings
select ...
settings
max_threads = 8,
max_memory_usage = 10000000000,
max_bytes_before_external_group_by = 5000000000;Comment Doctrine
Comments are for the person reading and reviewing the SQL. Say why a filter, join, or CTE exists, and anything that is not obvious. Write the reason as a plain sentence — no WHY: label.
Keep the work plan out of comments: no phases, plan steps, ticket numbers, or words like "for now", "temporary", or "later". Migrations attract this rot the most. Those notes go stale and someone has to find and delete them. Unfinished work belongs in plan.md.
Non-trivial queries, views, migrations, and SQL functions get a header comment above them: one line saying what it is for, plus the cost driver. Do not write big-O on SQL — the query planner picks the plan, so an O(...) bound would be a lie. Name the real driver instead: memory limit, group by cardinality, join build-side size, or parts/partitions scanned. This header is the SQL analogue of the big-O header in the code skills.
-- daily revenue rollup feeding the finance dashboard.
-- cost driver: group by cardinality (day x event_type, ~30 x small) and parts scanned for the date range.
insert into daily_stats
select
toDate(created_at) as day,
event_type,
sumIf(amount, status = 'completed') as revenue -- only completed orders count toward revenue
from events
where created_at >= today() - 30
group by day, event_type;Editing counts too. When you change a query, migration, or function, keep the comments already there, fix any your change made wrong, and add the missing header, cost note, or reason. Do this only for SQL you add or change, not for nearby SQL you did not touch.
Before Committing (Self-Review)
Schema
- Sort key matches primary query pattern
- Low-cardinality columns first in sort key
LowCardinality(String)for columns with < ~10K distinct valuesNullableavoided — use defaults instead- Partition by month (lifecycle), not for query performance
- TTL set for data retention policy
- Engine chosen intentionally (MergeTree, Replacing, Aggregating)
Queries
- Only needed columns selected (never
select *) finalused with ReplacingMergeTree when deduplication needed- Approximate functions where exact isn't required
-Ifcombinators for conditional aggregation- JOINs minimized — dictionaries or denormalization preferred
argMax/argMinfor latest-value patterns instead of subqueries
Inserts
- Batch size >= 10K rows per insert
async_insert = 1, wait_for_async_insert = 1for high-frequency small inserts- No single-row insert loops
Materialized Views
-State/-Mergecombinators used correctly- Target table uses AggregatingMergeTree
- Source → MV → target pipeline tested end-to-end: failing assertion first — insert fixture rows, assert
countMerge/uniqMergeagainst hand-computed values (red), wire the MV (green), then mutate a fixture row to confirm the assertion can fail (vacuous check)
Performance
explain indexes = 1(granule pruning) andexplain pipelinereviewed for non-trivial queries- Skipping index, then projection, considered for queries off the sort key
LowCardinalityon string columns with low distinct count
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

