ClickHouse quickstart: OLAP в compose
ClickHouse — columnar OLAP-БД для аналитики. Junior DE встречает её в трёх контекстах: (1) как target для агрегатов из Spark/Airflow pipeline’а, (2) как backend для дашбордов (Grafana, Superset), (3) как streaming-sink из Kafka через Kafka Engine. Все три кейса требуют локального стенда — и для этого ClickHouse прекрасно живёт в Docker.
В этом уроке поднимем ClickHouse в compose, настроим init-схему, подключим clickhouse-client, и в конце покажем killer-feature: подписку на Kafka topic напрямую из ClickHouse без посредников.
Брокеры, Топики, Партиции — базовая архитектура Kafka
Минимальный compose
services:
clickhouse:
image: clickhouse/clickhouse-server:24.10
container_name: clickhouse
ports:
- "8123:8123" # HTTP
- "9000:9000" # native TCP
- "9009:9009" # interserver (для кластера)
environment:
CLICKHOUSE_USER: default
CLICKHOUSE_PASSWORD: ''
CLICKHOUSE_DB: analytics
CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT: 1
ulimits:
nofile:
soft: 262144
hard: 262144
volumes:
- ch-data:/var/lib/clickhouse
- ch-logs:/var/log/clickhouse-server
- ./init:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8123/ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- ch-net
volumes:
ch-data:
ch-logs:
networks:
ch-net:
Что важно:
ulimits.nofile— это критично. ClickHouse открывает тысячи file descriptors (один на каждый парт таблицы). Дефолтный лимит Docker (1024) приведёт к ошибкам типаToo many open files. 262144 — рекомендация документации.- Три порта: 8123 (HTTP API, для curl/JDBC/HTTP-клиентов), 9000 (native TCP, для clickhouse-client и JDBC), 9009 (interserver replication — не нужен для single-node, но image слушает).
CLICKHOUSE_DB: analytics— создаёт БД analytics при первом старте../init:/docker-entrypoint-initdb.d— bind mount. ClickHouse при первом запуске выполняет все*.sqlфайлы из этой директории. Это место для DDL твоей схемы.
ВНИМАНИЕ: init-скрипты из /docker-entrypoint-initdb.d выполняются ТОЛЬКО при первой инициализации (когда volume пустой). Если ты добавил .sql после первого запуска — он не выполнится. Решения: (1) docker compose down -v и снова up (потеряешь данные), (2) clickhouse-client < init/02-new.sql напрямую (применить вручную), (3) использовать миграции (golang-migrate, dbt и т.д.).
Init schema
Создай файл ./init/01-schema.sql:
CREATE TABLE IF NOT EXISTS analytics.events
(
event_time DateTime,
user_id UInt64,
event_type LowCardinality(String),
properties String,
ingested_at DateTime DEFAULT now()
)
ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (event_time, user_id);
CREATE TABLE IF NOT EXISTS analytics.events_hourly
(
hour DateTime,
event_type LowCardinality(String),
cnt UInt64,
unique_users UInt64
)
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (hour, event_type);
Несколько ClickHouse-патернов, которые junior должен запомнить:
MergeTree()— основной engine. Колоночное хранение, сортировка поORDER BY-ключу, партиции поPARTITION BY. Базовый строительный блок.LowCardinality(String)— оптимизация для колонок с малой кардинальностью (типы событий, страны, статусы). ClickHouse строит словарь и хранит индексы — ускорение GROUP BY в десятки раз.PARTITION BY toYYYYMM(event_time)— каждый месяц отдельная партиция. Облегчает retention (DROP PARTITION) и speed-up filter’ов.SummingMergeTree— engine для агрегатов. Когда есть две строки с одинаковым ORDER BY-ключом, ClickHouse при merge суммирует UInt-колонки. Удобно для realtime-агрегации.
Подключение клиента
Два способа:
1) clickhouse-client внутри контейнера
docker compose exec clickhouse clickhouse-client --host clickhouse -u default
# Внутри REPL:
clickhouse :) SHOW DATABASES;
clickhouse :) USE analytics;
clickhouse :) SHOW TABLES;
clickhouse :) DESCRIBE TABLE events;
clickhouse :) SELECT count() FROM events;
clickhouse :) exit;
--host clickhouse тут не обязательно (внутри контейнера localhost тоже работает), но это привычка для consistency: тот же флаг работает и снаружи.
2) HTTP API через curl с хоста
curl 'http://localhost:8123/' --data-binary "SELECT version()"
# 24.10.2.80
curl 'http://localhost:8123/?database=analytics' \
--data-binary "INSERT INTO events VALUES (now(), 42, 'click', '{}', now())"
curl 'http://localhost:8123/?database=analytics' \
--data-binary "SELECT * FROM events LIMIT 5 FORMAT JSONEachRow"
HTTP-API подходит для quick-scripts и BI-инструментов. Для bulk-insert (миллионы строк) используй native TCP — он эффективнее.
INSERT и базовый SELECT
-- Внутри clickhouse-client
INSERT INTO analytics.events (event_time, user_id, event_type, properties) VALUES
(now() - INTERVAL 5 MINUTE, 1, 'click', '{"button": "buy"}'),
(now() - INTERVAL 4 MINUTE, 1, 'view', '{"page": "/home"}'),
(now() - INTERVAL 3 MINUTE, 2, 'click', '{"button": "subscribe"}'),
(now() - INTERVAL 2 MINUTE, 3, 'view', '{"page": "/pricing"}'),
(now() - INTERVAL 1 MINUTE, 2, 'click', '{"button": "buy"}');
SELECT event_type, count() AS cnt
FROM analytics.events
GROUP BY event_type
ORDER BY cnt DESC;
-- event_type | cnt
-- click | 3
-- view | 2
ClickHouse оптимизирован для аналитических запросов: GROUP BY с millions/billions строк проходит в десятки раз быстрее, чем в Postgres. Но он плох для частых UPDATE и DELETE — это OLAP, а не OLTP.
Kafka Engine: streaming в ClickHouse
Killer-feature ClickHouse для DE — Kafka Engine: создаёшь “таблицу”, которая на самом деле является consumer’ом Kafka-topic’а. Каждый SELECT забирает новые сообщения, и через Materialized View ты автоматически переливаешь их в обычную MergeTree-таблицу.
Допустим, у тебя есть Kafka из урока 16-02 с topic events. Подключим ClickHouse:
-- 1) Псевдо-таблица: consumer Kafka topic
CREATE TABLE analytics.events_kafka
(
event_time DateTime,
user_id UInt64,
event_type String,
properties String
)
ENGINE = Kafka()
SETTINGS
kafka_broker_list = 'kafka:9092',
kafka_topic_list = 'events',
kafka_group_name = 'clickhouse-events',
kafka_format = 'JSONEachRow',
kafka_num_consumers = 1;
-- 2) Materialized View: автоматически переливает из Kafka в обычную таблицу
CREATE MATERIALIZED VIEW analytics.events_mv TO analytics.events AS
SELECT
event_time,
user_id,
event_type,
properties
FROM analytics.events_kafka;
Что произошло:
events_kafka— это не таблица с данными, а консьюмер. ClickHouse подключается к Kafka наkafka:9092(по DNS-имени в compose-сети) и читает с offset’а.events_mv— Materialized View. Каждый раз, когдаevents_kafkaзабирает новую партию из Kafka, MV пересчитывает свой SELECT и пишет результат вevents.- В итоге ты получаешь streaming-pipeline без Airflow, без Spark Streaming. Просто SQL.
Теперь любой producer (включая kafka-console-producer.sh из прошлого урока) пишет в events topic JSON-сообщения, и через несколько секунд они появляются в analytics.events.
Чтобы Kafka Engine работал, ClickHouse и Kafka должны быть в одной compose-сети. Объедини через external networks или подключи оба сервиса к общей de-net. См. урок 16-05 про networking-патерны.
Реальный DE-кейс: streaming user events
Допустим, твоё web-приложение шлёт события в Kafka. Ты хочешь:
- Сохранять все события “сырыми” в ClickHouse для долгого retention.
- Агрегировать в hourly-bucket’ах для дашборда.
Pipeline:
-- Сырые события из Kafka -> events
-- (уже сделали выше: events_kafka + events_mv)
-- Hourly агрегаты: ещё одна MaterializedView
CREATE MATERIALIZED VIEW analytics.events_hourly_mv
TO analytics.events_hourly AS
SELECT
toStartOfHour(event_time) AS hour,
event_type,
count() AS cnt,
uniqExact(user_id) AS unique_users
FROM analytics.events
GROUP BY hour, event_type;
Теперь у тебя:
events_kafka— псевдо-таблица, читает Kafkaevents— MergeTree со всеми сырыми событиямиevents_hourly— SummingMergeTree с агрегатами по часу
И всё это без единой строчки внешнего кода. Только SQL и три MV.
Подключение к Grafana / BI
ClickHouse HTTP API на 8123 — это всё, что нужно для Grafana. Источник данных:
URL: http://clickhouse:8123 (внутри compose)
URL: http://localhost:8123 (с хоста)
Database: analytics
User: default
Password: (пусто)
Запросы в Grafana — обычный ClickHouse SQL:
SELECT
hour AS time,
event_type,
cnt
FROM analytics.events_hourly
WHERE hour > now() - INTERVAL 24 HOUR
ORDER BY hour;
Попробуй сам
# 1. Подготовь init/01-schema.sql (см. выше)
mkdir -p init
# Создай файл init/01-schema.sql вручную
# 2. Запусти
docker compose up -d
# 3. Проверь
docker compose exec clickhouse clickhouse-client \
--query "SHOW TABLES FROM analytics"
# events
# events_hourly
# 4. INSERT тестовые данные
docker compose exec clickhouse clickhouse-client --query "
INSERT INTO analytics.events VALUES
(now(), 1, 'click', '{}', now()),
(now(), 2, 'view', '{}', now()),
(now(), 1, 'view', '{}', now())"
# 5. SELECT через HTTP
curl 'http://localhost:8123/?database=analytics' \
--data-binary "SELECT count() FROM events"
# 6. Bulk insert большого dataset'а (1M строк) для теста производительности
docker compose exec clickhouse clickhouse-client --query "
INSERT INTO analytics.events
SELECT
now() - intDiv(number, 1000) * 60,
number % 1000,
['click', 'view', 'scroll'][1 + number % 3],
'{}',
now()
FROM numbers(1000000)"
# 7. Тяжёлый аналитический запрос
docker compose exec clickhouse clickhouse-client --query "
SELECT event_type, count(), uniqExact(user_id)
FROM analytics.events
GROUP BY event_type"
# 0.05 секунды на 1M строк -- ClickHouse в действии
# 8. Cleanup
docker compose down -v
Что ещё знать
- Cluster mode: для prod ClickHouse деплоится в кластере с replication через Zookeeper или Keeper. compose-файлы для кластера сложнее (3+ shards, replicas). В compose обычно одна нода.
- MinIO как backend: ClickHouse умеет хранить парты в S3 (
s3disk type). Это снижает стоимость для холодных данных. - ProxySQL / chproxy: на проде перед ClickHouse часто ставят chproxy для query routing, rate limiting, user management.
- Backup:
clickhouse-backup— утилита для бэкапов парт-файлов в S3.
Но это всё за пределами junior-роли. На данном этапе достаточно: compose -> init.sql -> clickhouse-client -> SELECT.