Client (clickhouse-client, HTTP API, приложение)
→ ClickHouse Server
→ Database
→ Table (MergeTree engine)
→ Parts (куски данных на диске, отсортированные по ORDER BY)
→ Columns (каждый столбец хранится отдельно, сжатый)
Колоночное хранение: данные каждого столбца хранятся отдельно. Запрос SELECT city, COUNT(*) ... GROUP BY city читает только столбец city, а не всю строку. На аналитических запросах это на порядки быстрее строкового хранения.
MergeTree: основное семейство движков таблиц. Данные вставляются порциями (parts), которые фоново сливаются (merge). Отсюда и название.
Part: неизменяемый кусок данных на диске. INSERT создаёт новый part. Фоновые merge-и объединяют мелкие parts в крупные.
Primary Key / ORDER BY: определяет физический порядок данных внутри part-а. Это НЕ уникальный ключ (дубликаты допускаются). Используется для пропуска гранул при чтении (sparse index).
Granule: минимальная единица чтения. По умолчанию 8192 строки. Sparse index хранит значения первой строки каждой гранулы.
Шардирование: горизонтальное разделение данных между серверами.
Репликация: копии данных на разных серверах для отказоустойчивости.
OLTP (PostgreSQL, MySQL) OLAP (ClickHouse)
───────────────────────── ─────────────────────
Много мелких операций Мало тяжёлых аналитических запросов
INSERT/UPDATE/DELETE по одной Batch INSERT, миллионы строк за раз
Строковое хранение Колоночное хранение
Индексы на каждый запрос Sparse index + сканирование столбцов
Нормализация (3NF) Денормализация (широкие таблицы)
Транзакции (ACID) Eventual consistency
Миллисекунды на точечный запрос Миллисекунды на агрегацию миллиардов строк
Используй UInt* / Int* минимальной ширины, это влияет на сжатие и скорость.
Строки
Тип
Описание
String
произвольная строка байт (основной тип)
FixedString(N)
строка ровно N байт (дополняется нулями)
LowCardinality(String)
словарное кодирование, для столбцов с малым числом уникальных значений (status, country)
Enum8 / Enum16
перечисление: хранится число, отображается строка
LowCardinality(String) является одним из главных приёмов оптимизации. Если в столбце < 10000 уникальных значений, оборачивай.
Дата и время
Тип
Описание
Пример
Date
дата (2 байта, с 1970)
2025-03-15
Date32
дата (4 байта, расширенный диапазон)
2025-03-15
DateTime
дата + время, секунды (4 байта)
2025-03-15 14:30:00
DateTime64(3)
дата + время с точностью (8 байт)
2025-03-15 14:30:00.123
DateTime хранит UTC timestamp (4 байта). Часовой пояс используется только для отображения: DateTime('Europe/Moscow').
Остальные
Тип
Описание
UUID
128-бит UUID
IPv4 / IPv6
IP-адреса (компактнее String)
Array(T)
массив
Tuple(T1, T2, ...)
кортеж
Map(K, V)
словарь
Nested(col1 T1, col2 T2)
вложенная структура (= несколько параллельных массивов)
Nullable(T)
допускает NULL (но замедляет, так как создаёт отдельный bitmask-столбец)
JSON
полуструктурированные данные (experimental)
Nullable: избегай без необходимости. Каждый Nullable-столбец создаёт дополнительный файл с bitmask и замедляет запросы. Используй пустые строки, 0 или дефолтные значения.
DDL: определение структуры
CREATE TABLE
-- Базовая аналитическая таблица
CREATETABLE events
(
event_date Date,
event_time DateTime,
user_id UInt64,
event_type LowCardinality(String),
page_url String,
duration_ms UInt32,
metadata Map(String, String)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_date) -- партиция по месяцу
ORDERBY (event_type, user_id, event_time) -- сортировка и sparse index
TTL event_date + INTERVAL 6MONTH-- автоудаление через 6 месяцев
SETTINGS index_granularity =8192; -- строк в грануле (по умолчанию)
-- Таблица логов
CREATETABLE logs
(
timestamp DateTime64(3),
level Enum8('DEBUG'=0, 'INFO'=1, 'WARN'=2, 'ERROR'=3),
service LowCardinality(String),
message String,
trace_id UUID,
attrs Map(String, String)
)
ENGINE = MergeTree()
PARTITION BY toYYYYMMDD(timestamp)
ORDERBY (service, level, timestamp)
TTL toDate(timestamp) + INTERVAL 30DAYSETTINGS
index_granularity =8192,
ttl_only_drop_parts =1; -- удалять целые parts, а не строки
ORDER BY: ключевое решение
ORDER BY определяет физический порядок данных. От него зависит скорость запросов.
-- Правило: слева — столбцы с низкой кардинальностью, фильтры по равенству
-- справа — столбцы с высокой кардинальностью, диапазоны
ORDERBY (event_type, user_id, event_time)
-- ^low card ^filter by = ^range filter
-- Если большинство запросов: WHERE user_id = X AND event_time BETWEEN ...
ORDERBY (user_id, event_time)
-- Если запросы разные — компромисс или несколько materialized views
Запросы эффективны, если фильтр по WHERE идёт по префиксу ORDER BY (как в PostgreSQL с составными индексами).
PARTITION BY
PARTITION BY toYYYYMM(event_date) -- по месяцу (самый частый вариант)
PARTITION BY toYYYYMMDD(event_date) -- по дню (если очень много данных)
PARTITION BY (region, toYYYYMM(date)) -- составной ключ
-- Управление партициями
SELECT partition, name, rows, bytes_on_disk
FROMsystem.parts
WHEREtable='events'AND active;
-- Удалить партицию (мгновенно — удаляет файлы)
ALTERTABLE events DROP PARTITION '202401';
-- Отсоединить / присоединить (для бэкапа или переноса)
ALTERTABLE events DETACH PARTITION '202401';
ALTERTABLE events ATTACH PARTITION '202401';
Правило: не более ~1000 партиций на таблицу. Каждая партиция = директория на диске.
/dev/null для таблиц (полезно как источник для materialized views)
Memory
Данные в RAM (тесты, мелкие lookup-таблицы)
File / URL / S3
Чтение из внешних источников
ReplacingMergeTree
-- Дедупликация по ORDER BY ключу
-- При merge оставляет строку с максимальным ver
CREATETABLE users
(
id UInt64,
name String,
email String,
updated_at DateTime,
ver UInt64 -- версия
)
ENGINE = ReplacingMergeTree(ver)
ORDERBY id;
-- INSERT перезаписывает "логически" — физически старые строки удаляются при merge
INSERTINTO users VALUES (1, 'Alice', 'alice@old.com', now(), 1);
INSERTINTO users VALUES (1, 'Alice', 'alice@new.com', now(), 2);
-- Гарантированная дедупликация при чтении:
SELECT*FROM users FINALWHERE id =1;
-- FINAL — медленнее, применяет merge логику в query time
-- Или через подзапрос (часто эффективнее FINAL):
SELECT*FROM users
WHERE (id, ver) IN (SELECT id, max(ver) FROM users GROUPBY id);
AggregatingMergeTree
-- Для materialized views с агрегатами
CREATE MATERIALIZED VIEW events_daily_mv
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(event_date)
ORDERBY (event_date, event_type)
ASSELECT event_date,
event_type,
countState() AS events_count, -- не count(), а countState()!
uniqState(user_id) AS unique_users,
avgState(duration_ms) AS avg_duration
FROM events
GROUPBY event_date, event_type;
-- Чтение — через Merge-функции
SELECT event_date,
event_type,
countMerge(events_count) AS events,
uniqMerge(unique_users) AS users,
avgMerge(avg_duration) AS avg_dur
FROM events_daily_mv
GROUPBY event_date, event_type
ORDERBY event_date DESC;
DML: работа с данными
INSERT
-- Значения
INSERTINTO events (event_date, event_time, user_id, event_type, page_url, duration_ms)
VALUES ('2025-03-15', '2025-03-15 14:30:00', 42, 'pageview', '/home', 1500);
-- Несколько строк (рекомендуемый способ — batch insert)
INSERTINTO events VALUES ('2025-03-15', '2025-03-15 14:30:00', 42, 'pageview', '/home', 1500, {}),
('2025-03-15', '2025-03-15 14:31:00', 42, 'click', '/products', 200, {}),
('2025-03-15', '2025-03-15 14:32:00', 43, 'pageview', '/home', 800, {});
-- INSERT из SELECT
INSERTINTO events_archive
SELECT*FROM events WHERE event_date <'2024-01-01';
-- INSERT из файла через HTTP
curl 'http://localhost:8123/?query=INSERT+INTO+events+FORMAT+CSV'--data-binary @events.csv
curl 'http://localhost:8123/?query=INSERT+INTO+events+FORMAT+JSONEachRow'--data-binary @events.jsonl
-- INSERT из stdin через клиент
clickhouse-client --query "INSERT INTO events FORMAT CSV" < events.csv
cat events.jsonl | clickhouse-client --query "INSERT INTO events FORMAT JSONEachRow"
Правило batch insert: не делай много мелких INSERT-ов (по 1-10 строк), каждый создаёт part на диске. Батчи по 10000-1000000 строк, или не чаще 1 INSERT в секунду.
UPDATE и DELETE
ClickHouse не является OLTP-системой. UPDATE/DELETE представляют собой мутации (тяжёлые фоновые операции):
Мутации асинхронны, данные удаляются/обновляются не мгновенно. Для паттерна «обновляемые данные» используй ReplacingMergeTree или CollapsingMergeTree.
Удаление данных: правильные подходы
-- 1. TTL (лучший вариант для данных с ограниченным сроком жизни)
ALTERTABLE logs MODIFY TTL toDate(timestamp) + INTERVAL 90DAY;
-- 2. DROP PARTITION (мгновенно, если данные партиционированы по нужному ключу)
ALTERTABLE events DROP PARTITION '202301';
-- 3. TRUNCATE (очистить всю таблицу)
TRUNCATETABLE events;
SELECT: выборка данных
Основы
SELECT*FROM events LIMIT10;
SELECT event_type, count() FROM events GROUPBY event_type;
-- LIMIT BY — TOP N для каждой группы (уникальная фича ClickHouse)
SELECT user_id, event_type, event_time
FROM events
ORDERBY event_time DESCLIMIT3BY user_id; -- последние 3 события для каждого пользователя
-- WITH — CTE (как в PostgreSQL)
WITH daily AS (
SELECT event_date, count() AS cnt
FROM events
GROUPBY event_date
)
SELECTavg(cnt) FROM daily;
-- SAMPLE — случайная выборка (быстрый approximate на больших данных)
SELECTcount() FROM events SAMPLE 0.1; -- ~10% данных
SELECTavg(duration_ms) FROM events SAMPLE 1000000; -- ~1M строк
Агрегатные функции
-- Стандартные
SELECTcount(), -- количество строк (без аргумента!)
countIf(duration_ms >1000), -- условный count
sum(duration_ms),
avg(duration_ms),
min(event_time),
max(event_time),
any(page_url), -- любое значение из группы (быстро)
anyLast(page_url) -- последнее вставленное
FROM events;
-- Приближённые уникальные (быстро, ~2% погрешность)
SELECT uniq(user_id) FROM events; -- HyperLogLog (основной)
SELECT uniqExact(user_id) FROM events; -- точный (медленнее, больше памяти)
SELECT uniqCombined(user_id) FROM events; -- adaptive
-- Квантили
SELECT quantile(0.5)(duration_ms) AS median,
quantile(0.95)(duration_ms) AS p95,
quantile(0.99)(duration_ms) AS p99,
quantiles(0.5, 0.95, 0.99)(duration_ms) AS quantiles_arr
FROM events;
-- Массивы из групп
SELECT event_type,
groupArray(user_id) AS user_ids, -- все значения в массив
groupArray(10)(user_id) AS first_10_users, -- первые 10
groupUniqArray(user_id) AS unique_user_ids,
groupArraySorted(5)(duration_ms) AS top5_durations -- топ 5 отсортированных
FROM events
GROUPBY event_type;
-- Гистограмма
SELECT histogram(10)(duration_ms) FROM events;
-- TopK — приближённые топ значения
SELECT topK(10)(page_url) FROM events; -- топ 10 URL
-- -If комбинатор — условные агрегаты (работает с любой агрегатной функцией)
SELECT countIf(event_type ='pageview') AS pageviews,
countIf(event_type ='click') AS clicks,
uniqIf(user_id, event_type ='purchase') AS buyers,
avgIf(duration_ms, duration_ms >0) AS avg_nonzero
FROM events;
-- -Array комбинатор — агрегат по массиву
SELECT sumArray([1, 2, 3]); -- 6
SELECT groupUniqArrayArray(tags) FROM posts; -- все уникальные теги
SELECTmap('key1', 'val1', 'key2', 'val2'),
metadata['browser'], -- доступ по ключу
mapKeys(metadata), -- все ключи
mapValues(metadata), -- все значения
mapContains(metadata, 'browser'); -- ключ существует?
-- Фильтр по Map
SELECT*FROM events WHERE metadata['browser'] ='Chrome';
JOIN
-- ClickHouse по умолчанию использует Hash Join
-- Правая таблица загружается в память!
-- Обычный JOIN
SELECT e.event_type, u.name
FROM events e
JOIN users u ON e.user_id = u.id;
-- LEFT JOIN
SELECT e.*, u.name
FROM events e
LEFTJOIN users u ON e.user_id = u.id;
-- JOIN с подсказкой алгоритма
SELECT*FROM big_table a
JOIN small_table b ON a.key= b.keySETTINGS join_algorithm ='hash'; -- hash (default), partial_merge, full_sorting_merge, grace_hash
-- GLOBAL JOIN — для Distributed таблиц (правая таблица рассылается на все шарды)
SELECT*FROM distributed_events e
GLOBALJOIN users u ON e.user_id = u.id;
-- ASOF JOIN — присоединить ближайшее по времени значение
SELECT t.timestamp, t.symbol, t.price,
q.bid, q.ask
FROM trades t
ASOF LEFTJOIN quotes q
ON t.symbol = q.symbol AND t.timestamp>= q.timestamp;
Правило: правая таблица в JOIN загружается в память. Ставь маленькую таблицу справа.
Словари (Dictionaries)
Lookup-таблицы целиком в памяти. Замена JOIN для справочников:
CREATEDICTIONARY user_dict
(
id UInt64,
name String,
email String,
role String
)
PRIMARYKEY id
SOURCE(CLICKHOUSE(TABLE'users' DB 'default'))
LAYOUT(HASHED()) -- вся таблица в хешмапе в RAM
LIFETIME(MIN300MAX600); -- обновление каждые 5-10 минут
-- Использование (вместо JOIN)
SELECT user_id,
dictGet('user_dict', 'name', user_id) AS user_name,
dictGet('user_dict', 'role', user_id) AS user_role
FROM events;
-- Источники словарей
SOURCE(CLICKHOUSE(TABLE'users' DB 'default'))
SOURCE(MYSQL(HOST'db' PORT 3306USER'reader' PASSWORD 'pass'TABLE'users' DB 'app'))
SOURCE(POSTGRESQL(HOST'db' PORT 5432USER'reader'TABLE'users' DB 'app'))
SOURCE(HTTP(URL 'https://api.example.com/users.csv' FORMAT 'CSV'))
SOURCE(FILE(PATH '/data/users.csv' FORMAT 'CSV'))
-- Layouts
LAYOUT(FLAT()) -- массив (если ключ UInt64 и не разреженный)
LAYOUT(HASHED()) -- хешмап (основной)
LAYOUT(CACHE(SIZE_IN_CELLS 100000)) -- кеш (для больших справочников, lazy loading)
LAYOUT(RANGE_HASHED()) -- диапазонный (для данных с date_from/date_to)
LAYOUT(COMPLEX_KEY_HASHED()) -- составной ключ
Materialized Views
Автоматическая трансформация данных при INSERT. Основной механизм для преагрегации:
-- Исходная таблица
CREATETABLE raw_events
(
timestamp DateTime,
user_id UInt64,
event LowCardinality(String),
value Float64
)
ENGINE = MergeTree()
ORDERBY (event, timestamp);
-- Целевая таблица для агрегатов
CREATETABLE events_hourly
(
hour DateTime,
event LowCardinality(String),
count SimpleAggregateFunction(sum, UInt64),
users AggregateFunction(uniq, UInt64),
total SimpleAggregateFunction(sum, Float64),
avg_val AggregateFunction(avg, Float64)
)
ENGINE = AggregatingMergeTree()
ORDERBY (event, hour);
-- Materialized View — триггер на INSERT в raw_events
CREATE MATERIALIZED VIEW events_hourly_mv
TO events_hourly -- записывает результат в events_hourly
ASSELECT toStartOfHour(timestamp) AS hour,
event,
count() AScount,
uniqState(user_id) AS users,
sum(value) AS total,
avgState(value) AS avg_val
FROM raw_events
GROUPBY hour, event;
-- Чтение из целевой таблицы
SELECT hour,
event,
sum(count) AS total_events,
uniqMerge(users) AS unique_users,
sum(total) AS total_value,
avgMerge(avg_val) AS avg_value
FROM events_hourly
WHERE hour >= today()
GROUPBY hour, event
ORDERBY hour DESC;
INSERT INTO raw_events → MV автоматически агрегирует → INSERT INTO events_hourly
(происходит при каждом INSERT)
Паттерн: хранить сырые данные в основной таблице + materialized views для частых агрегаций. Запросы к MV в 100-1000x быстрее, чем к сырым данным.
Форматы данных
ClickHouse поддерживает десятки форматов для ввода/вывода:
-- Вывод в разных форматах
SELECT*FROM events FORMAT JSON;
SELECT*FROM events FORMAT JSONEachRow; -- одна строка JSON на запись (для streaming)
SELECT*FROM events FORMAT CSV;
SELECT*FROM events FORMAT CSVWithNames;
SELECT*FROM events FORMAT TSV;
SELECT*FROM events FORMAT Parquet;
SELECT*FROM events FORMAT Arrow;
SELECT*FROM events FORMAT Pretty; -- для человека
SELECT*FROM events FORMAT Vertical; -- столбцами
-- Вставка из разных форматов
INSERTINTO events FORMAT CSV
2025-03-15,2025-03-1514:30:00,42,pageview,/home,1500,{};
INSERTINTO events FORMAT JSONEachRow
{"event_date": "2025-03-15", "event_time": "2025-03-15 14:30:00", "user_id": 42, "event_type": "pageview", "page_url": "/home", "duration_ms": 1500, "metadata": {}};
-- План запроса
EXPLAINSELECTcount() FROM events WHERE event_type ='click';
-- Подробный план с pipeline
EXPLAIN PIPELINE SELECT event_type, count() FROM events GROUPBY event_type;
-- План с оценкой строк
EXPLAIN ESTIMATE SELECT*FROM events WHERE event_date = today();
-- Реальные метрики после выполнения
SELECT query,
read_rows,
read_bytes,
result_rows,
memory_usage,
query_duration_ms
FROMsystem.query_log
WHEREtype='QueryFinish'ORDERBY event_time DESCLIMIT10;
Системные таблицы для мониторинга
-- Активные запросы
SELECT query_id, user, elapsed, query FROMsystem.processes;
-- Убить запрос
KILL QUERY WHERE query_id ='xxx';
-- Размер таблиц
SELECTtable,
formatReadableSize(sum(bytes_on_disk)) ASsize,
sum(rows) ASrows,
count() AS parts
FROMsystem.parts
WHERE active ANDdatabase='default'GROUPBYtableORDERBYsum(bytes_on_disk) DESC;
-- Размер столбцов (какой столбец сколько занимает)
SELECTcolumn,
formatReadableSize(sum(column_data_compressed_bytes)) AS compressed,
formatReadableSize(sum(column_data_uncompressed_bytes)) AS uncompressed,
round(sum(column_data_uncompressed_bytes) /sum(column_data_compressed_bytes), 2) AS ratio
FROMsystem.parts_columns
WHERE active ANDtable='events'ANDdatabase='default'GROUPBYcolumnORDERBYsum(column_data_compressed_bytes) DESC;
-- Статистика merge-ей
SELECT*FROMsystem.merges WHEREdatabase='default';
-- Прогресс мутаций
SELECT*FROMsystem.mutations WHERENOT is_done;
-- Логи запросов (медленные запросы)
SELECT query_duration_ms,
read_rows,
formatReadableSize(read_bytes) ASread,
formatReadableSize(memory_usage) AS memory,
query
FROMsystem.query_log
WHEREtype='QueryFinish'AND query_duration_ms >1000ORDERBY query_duration_ms DESCLIMIT20;
-- Ошибки
SELECT*FROMsystem.query_log WHEREtype='ExceptionWhileProcessing'ORDERBY event_time DESCLIMIT10;
Оптимизация запросов
-- 1. Фильтр по ORDER BY ключу (использует sparse index)
-- ХОРОШО: event_type — первый столбец в ORDER BY
SELECTcount() FROM events WHERE event_type ='click';
-- ПЛОХО: duration_ms не в ORDER BY — полный скан столбца
SELECTcount() FROM events WHERE duration_ms >1000;
-- 2. PREWHERE — автоматическая оптимизация (или явная)
-- Сначала читает столбец фильтра, потом остальные только для подходящих строк
SELECT*FROM events PREWHERE event_type ='click'WHERE duration_ms >1000;
-- (обычно ClickHouse делает это автоматически)
-- 3. Избегай SELECT * — читай только нужные столбцы
SELECT event_type, count() FROM events GROUPBY event_type; -- быстро
SELECT*FROM events; -- медленно (все столбцы)
-- 4. Используй LowCardinality
-- До: String → 60 байт на значение
-- После: LowCardinality(String) → ~1-2 байта на значение (словарная кодировка)
-- 5. FINAL — избегай на больших таблицах
-- Вместо: SELECT * FROM replacing_table FINAL
-- Лучше:
SELECT*FROM replacing_table
WHERE (key, ver) IN (SELECTkey, max(ver) FROM replacing_table GROUPBYkey);
Индексы пропуска данных (Data Skipping Indexes)
-- Дополнительные индексы (не заменяют ORDER BY, а дополняют)
ALTERTABLE events ADDINDEX idx_url page_url TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 4;
ALTERTABLE events MATERIALIZE INDEX idx_url;
-- Типы индексов пропуска
-- minmax — диапазон значений в грануле
-- set(N) — множество уникальных значений (до N)
-- bloom_filter — вероятностный (подходит для равенства и IN)
-- tokenbf_v1 — bloom filter по токенам (для поиска подстрок)
-- ngrambf_v1 — bloom filter по n-граммам
CREATETABLE logs
(
timestamp DateTime,
message String,
level LowCardinality(String),
INDEX idx_level levelTYPEset(10) GRANULARITY 4,
INDEX idx_message message TYPE tokenbf_v1(32768, 3, 0) GRANULARITY 4)
ENGINE = MergeTree()
ORDERBYtimestamp;
Импорт и экспорт данных
Из файлов
# CSVclickhouse-client --query "INSERT INTO events FORMAT CSVWithNames" < events.csv
# JSON Linesclickhouse-client --query "INSERT INTO events FORMAT JSONEachRow" < events.jsonl
# Parquetclickhouse-client --query "INSERT INTO events FORMAT Parquet" < events.parquet
# С gzip на летуgunzip -c events.csv.gz | clickhouse-client --query "INSERT INTO events FORMAT CSVWithNames"# Экспортclickhouse-client --query "SELECT * FROM events FORMAT CSVWithNames" > events.csv
clickhouse-client --query "SELECT * FROM events FORMAT Parquet" > events.parquet
clickhouse-client --query "SELECT * FROM events FORMAT JSONEachRow" | gzip > events.jsonl.gz
-- На уровне запроса
SELECTcount() FROM events
SETTINGS max_threads =4, max_memory_usage =10000000000;
-- На уровне сессии
SET max_threads =4;
SET max_memory_usage =10000000000; -- 10GB
SET max_execution_time =60; -- секунды
SET max_rows_to_read =1000000000;
SET join_algorithm ='grace_hash'; -- для больших JOIN
-- Полезные настройки
SET optimize_read_in_order =1; -- использовать ORDER BY таблицы
SET optimize_aggregation_in_order =1; -- GROUP BY по ORDER BY ключу
SET allow_experimental_lightweight_delete =1; -- лёгкие DELETE
SET input_format_allow_errors_ratio =0.01; -- допустить 1% ошибок при импорте
Паттерны
Аналитические запросы
-- Retention (когортный анализ)
WITH first_events AS (
SELECT user_id, min(event_date) AS cohort_date
FROM events
WHERE event_type ='signup'GROUPBY user_id
)
SELECT cohort_date,
count() AS cohort_size,
countIf(dateDiff('day', cohort_date, return_date) BETWEEN1AND1) AS day1,
countIf(dateDiff('day', cohort_date, return_date) BETWEEN7AND7) AS day7,
countIf(dateDiff('day', cohort_date, return_date) BETWEEN30AND30) AS day30
FROM first_events fe
LEFTJOIN (
SELECTDISTINCT user_id, event_date AS return_date
FROM events
) ret ON fe.user_id = ret.user_id
GROUPBY cohort_date
ORDERBY cohort_date;
-- Funnel (воронка)
SELECTcount() AS step1_view,
countIf(step2_time >0) AS step2_cart,
countIf(step3_time >0) AS step3_purchase
FROM (
SELECT user_id,
min(if(event_type ='page_view', event_time, NULL)) AS step1_time,
min(if(event_type ='add_to_cart'AND event_time > step1_time, event_time, NULL)) AS step2_time,
min(if(event_type ='purchase'AND event_time > step2_time, event_time, NULL)) AS step3_time
FROM events
WHERE event_date = today()
GROUPBY user_id
);
-- windowFunnel — встроенная функция для воронок
SELECTlevel,
count() AS users
FROM (
SELECT user_id,
windowFunnel(86400)(event_time, event_type ='page_view', event_type ='add_to_cart', event_type ='purchase') ASlevelFROM events
WHERE event_date >= today() -7GROUPBY user_id
)
GROUPBYlevelORDERBYlevel;
Временные ряды
-- Заполнение пропусков (WITH FILL)
SELECT toStartOfHour(event_time) AS hour,
count() AS events
FROM events
WHERE event_date = today()
GROUPBY hour
ORDERBY hour WITH FILL
FROM toStartOfDay(now())
TO now()
STEP INTERVAL 1 HOUR;
-- Скользящее среднее
SELECT event_date,
events_count,
avg(events_count) OVER (ORDERBY event_date ROWSBETWEEN6 PRECEDING ANDCURRENTROW) AS ma_7d
FROM (
SELECT event_date, count() AS events_count
FROM events
GROUPBY event_date
)
ORDERBY event_date;
-- Rate of change (изменение в процентах)
SELECT event_date,
events_count,
events_count / lagInFrame(events_count) OVER (ORDERBY event_date) -1AS change_pct
FROM (
SELECT event_date, count() AS events_count FROM events GROUPBY event_date
);
Дедупликация
-- ReplacingMergeTree + FINAL
SELECT*FROM users FINAL;
-- argMax — взять значение столбца для строки с максимальным другим столбцом
SELECT user_id,
argMax(name, updated_at) AS latest_name,
argMax(email, updated_at) AS latest_email,
max(updated_at) AS last_update
FROM users
GROUPBY user_id;
-- Распределённая таблица (прокси ко всем шардам)
CREATETABLE events_distributed ONCLUSTER'mycluster'AS events
ENGINE = Distributed('mycluster', 'default', 'events', user_id);
-- cluster database table sharding_key
-- INSERT в distributed → раскидывает по шардам по sharding_key
-- SELECT из distributed → запрос ко всем шардам → агрегация результатов
Бэкап
# Встроенный бэкап (ClickHouse 22.8+)clickhouse-client --query "BACKUP TABLE events TO Disk('backups', 'events_backup')"clickhouse-client --query "RESTORE TABLE events FROM Disk('backups', 'events_backup')"# Бэкап в S3clickhouse-client --query "BACKUP TABLE events TO S3('https://bucket.s3.amazonaws.com/backup/', 'key', 'secret')"# clickhouse-backup (утилита)clickhouse-backup create daily_backup
clickhouse-backup upload daily_backup
clickhouse-backup restore_remote daily_backup
Частые проблемы
Too many parts:
DB::Exception: Too many parts (N). Merges are processing significantly slower than inserts.
Слишком частые INSERT-ы. Каждый INSERT = новый part на диске. Решения:
Батчить INSERT-ы (10000+ строк за раз, не чаще 1/сек)
Использовать Buffer-движок или async_insert
Проверить: SELECT table, count() FROM system.parts WHERE active GROUP BY table ORDER BY count() DESC
-- Async insert — ClickHouse сам батчит мелкие вставки
SET async_insert =1;
SET wait_for_async_insert =0;
Запрос ест всю память:
-- Ограничить память на запрос
SET max_memory_usage =10000000000; -- 10GB
-- Использовать внешнюю сортировку/агрегацию (на диске, медленнее)
SET max_bytes_before_external_sort =5000000000;
SET max_bytes_before_external_group_by =5000000000;
JOIN жрёт память:
Правая таблица загружается в память. Решения:
Маленькую таблицу ставь справа
Использовать join_algorithm = 'grace_hash' или 'partial_merge'
Заменить JOIN на Dictionary
Предфильтровать данные подзапросом
Медленный запрос, что делать:
-- 1. Проверить, сколько строк читается
SELECTcount() FROM events WHERE<conditions>;
-- 2. Проверить, попадает ли фильтр в ORDER BY ключ
EXPLAIN indexes =1SELECT ... FROM events WHERE ...;
-- Ищи "Selected Parts" и "Selected Granules" — чем меньше, тем лучше
-- 3. Посмотреть, какие столбцы самые тяжёлые
SELECTcolumn, formatReadableSize(sum(column_data_compressed_bytes)) ASsizeFROMsystem.parts_columns
WHEREtable='events'AND active
GROUPBYcolumnORDERBYsum(column_data_compressed_bytes) DESC;
-- 4. Не используй SELECT * — укажи только нужные столбцы
-- 5. Добавь data skipping index, если фильтр не по ORDER BY
-- 6. Создай materialized view с преагрегацией
Nullable тормозит:
Каждый Nullable-столбец хранит дополнительный bitmask-файл. Решения:
Замени Nullable(String) на String с DEFAULT ''
Замени Nullable(UInt64) на UInt64 с DEFAULT 0
Используй Nullable только если семантика NULL принципиально важна
ClickHouse дедуплицирует INSERT-ы на уровне блоков (по хешу). Если вставить один и тот же блок дважды, он проигнорируется. Но если перемешать строки, будет дубликат.
-- Для ReplicatedMergeTree — встроенная дедупликация блоков
-- Для обычного MergeTree — нет автоматической дедупликации
-- Ручная дедупликация через ReplacingMergeTree + FINAL
-- Или через OPTIMIZE ... DEDUPLICATE
OPTIMIZE TABLE events DEDUPLICATE BY user_id, event_time;