Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → ClickHouse для разработчика: основы колоночной аналитики

ClickHouse глазами разработчика: колоночная модель, MergeTree и границы применимости

Зачем колоночная СУБД и когда ClickHouse оправдан: MergeTree-семейство, ORDER BY-ключ, батч-загрузка, материализованные представления и чего ClickHouse не умеет.

Прямой ответ

ClickHouse — колоночная OLAP-СУБД: данные хранятся по колонкам (агрегат по одной колонке из миллиарда строк читает только её — против «строчного» Postgres, читающего строки целиком — почему планы тяжелы), всё векторизовано и параллельно — типовые аналитические запросы на миллиардах строк — секунды. Когда он нужен разработчику: аналитика/метрики/логи/событийные данные, где запросы — «агрегации по большим срезам» (GROUP BY/sum/count/quantile по миллионам-миллиардам), вставка — потоковая батчами (события, метрики, клики), обновления — редки и не точечные. Когда не нужен: OLTP (точечные SELECT/UPDATE по ключу — не его игра), транзакционная целостность (нет полноценных ACID-транзакций на несколько таблиц — изоляция не про это), частые одиночные вставки (движок оптимизирован под батчи — «по одной строке в секунду» — антипаттерн, порождающий тысячи мелких parts), частые UPDATE/DELETE (мутации — тяжёлые фоновые переписывания, не «обновил строку»). База практики: MergeTree — семейство движков («сортированные части со слиянием»): ORDER BY (tenant_id, created_at) — первичный «индекс» (разреженный — данные физически отсортированы, запросы по префиксу ключа читают минимум гранул — аналог кластеризованного индекса), PARTITION BY по месяцу (отсечение старых срезов + дешёвое удаление — DROP PARTITION), батч-загрузка (10k+ строк на INSERT — норма; чаще и мельче — parts-шторм), материализованные представления (агрегаты на лету при вставке — «суммы по часам» считаются один раз), TTL-колонки/строки (автоудаление/перенос старого — retention как DDL). И честная граница с DuckDB: один аналитик на ноутбуке с файлами — DuckDB; много пользователей/потоковая нагрузка/террабайты/продакшен-сервис — ClickHouse.

Колоночная модель: почему быстро и что это меняет

Хранение: каждая колонка — отдельный файл (сжатый — однородные данные колонок сжимаются в разы лучше строчных — меньше IO; LZ4/ZSTD на выбор); чтение запроса SELECT category, sum(amount) FROM orders WHERE created_at >= ... — касание колонок category/amount/created_at (из 40 колонок таблицы — 3 — выигрыш ×10+ по IO), векторизованная обработка блоками (SIMD-операции над массивами значений — против построчной обработки), параллелизм на ядра. Следствия для разработчика: (1) широкие таблицы нормальны (100+ колонок — не страх — запрос платит только за тронутые колонки; денормализация поощряется — JOIN-ы в момент запроса дороже, чем «лишняя» колонка при вставке — звёздная схема уступает «плоскому широкому факту» — инверсия OLTP-интуиции); (2) COUNT(*)/агрегаты почти бесплатны (метаданные parts хранят min/max/числа строк по гранулам — отсечение до чтения); (3) точечный SELECT по ключу — не оптимален (чтение колонки целиком для одной строки — хотя при ключевом префиксе ORDER BY — приемлемо для админок; «тысячи точечных запросов в секунду» — нет); (4) JOIN — узкое место (правая таблица грузится в память — большие×большие JOIN — боль; решения: словаря/справочники маленькими (Dictionary-объекты), денормализация при ETL (пайплайн склеивает заранее), IN подзапросы вместо JOIN); (5) сжатие — аргумент дисковой экономики (логи/события — 5–20× сжатие против сырых).

MergeTree: ключ, партиции, загрузка

CREATE TABLE events (...) ENGINE = MergeTree() PARTITION BY toYYYYMM(created_at) ORDER BY (tenant_id, event_type, created_at) TTL created_at + INTERVAL 12 MONTH — разбор: ORDER BY — физическая сортировка = первичный индекс (разреженный: метки на гранулы по 8192 строки — запрос по префиксу ключа (tenant_id, event_type) прыгает по меткам — «индекс» без отдельной структуры; порядок колонок в ключе — по селективности и паттерну запросов — как выбор индексов, только физический); вторичные «индексы» — skip indexes (minmax/bloomfilter на колонку — отсечение гранул по неключевым фильтрам — полезны по факту запросов, не «на всякий»); PARTITION BY — месяц/день (отсечение в запросах по времени + DROP PARTITION для retention — мгновенно против DELETE-мутаций; слишком мелкое партиционирование — parts-множество — мера); загрузка: батчи 10k–1M строк (INSERT ... VALUES/SELECT, async_insert для мелких потоков — сервер сам батчит; «вставка по строке» из приложения — накапливать в очереди и лить батчами — Kafka/буфер-таблицы); parts-жизнь: каждый INSERT — part, фоновые слияния укрупняют (слишком частые вставки — слишком много parts — «too many parts»-ошибка — классика); материализованные представления — ENGINE = SummingMergeTree/AggregatingMergeTree + MV-триггер (вставка в основу — агрегаты дописываются — запросы к MV вместо пересчёта миллиардов — главный приём «дашборд за секунды»); мутации (ALTER UPDATE/DELETE) — фоновые переписывания parts — тяжелы, не для регулярного применения (исправления данных — ок, «обновлять профиль пользователя» — нет); ReplacingMergeTree/CollapsingMergeTree — дедупликация/отмены при слияниях (eventual — «схлопывание» не мгновенно — FINAL/argMax в запросах — знать семантику до выбора движка).

Эксплуатация: от ноутбука до кластера

Локальный старт: docker-контейнер (clickhouse-server + clickhouse-client — локальный стек — секунды), данные из parquet/CSV (INSERT SELECT FROM file(...) — DuckDB-подобный ad-hoc), HTTP-интерфейс (8123) и native (9000) — клиенты: clickhouse-connect (Python — официальный), интеграция pandas/polars (чтение в фрейм — аналитика в Python — гибридный поток: ClickHouse хранит, Python считает финал); из Python-приложения — httpx к HTTP-интерфейсу с параметрами (или connect-драйвер) — пулы/таймауты/ретраи — как к любому внешнему сервису. Продакшен: один сервер покрывает удивительно много (миллиарды строк, терабайты — вертикаль до очень высоких пределов — «кластер нужен сразу» — обычно нет); кластер (шардирование+реплики, Keeper для координации, Distributed-таблицы) — от исчерпания одной машины/требований HA (репликация ReplicatedMergeTree — ZooKeeper/ClickHouse Keeper — операционная стоимость заметна — паттерны отказоустойчивости применять); managed-сервисы (ClickHouse Cloud/облачные) — снимают Keeper/шардирование — стандартный путь команд без выделенных DBA; наблюдаемость — системные таблицы (system.parts/merges/query_log — «почему медленно/почему parts много» — там же), метрики в Prometheus (инструментация); бэкап — BACKUP-команды в S3 (хранилище); безопасность — пользователи/квоты/лимиты на запрос (max_memory_usage/max_execution_time per user — один «аналитический гений» с cartesian join не должен класть сервер — лимиты обязательны, как rate limit в API).

Практический сценарий: аналитика событий из Postgres в ClickHouse

Исходник: таблица events в Postgres — 800M строк (партицирована), продуктовые дашборды (агрегации по дням/фичам/когортам) — 30–90 с на запрос, аналитические сканы мешают OLTP даже на реплике (реплика одна — отчётные и «свежесть для чтения» конфликтуют). Решение: ClickHouse-сервер рядом (docker → позже managed); ETL: исторический перенос — партициями по месяцам (COPY → parquet → INSERT SELECT FROM file — 800M строк за 40 минут, 210 ГБ → 34 ГБ после сжатия — 6×); потоковая доставка — outbox-события → Kafka → ClickHouse Kafka-движок (батчи по 50k — доставка at-least-once — ReplacingMergeTree по (event_id) схлопывает дубли при слияниях — дашборды через argMax/FINAL где нужна точность); схема: ORDER BY (tenant_id, event_type, toStartOfHour(created_at)), PARTITION BY toYYYYMM, TTL 24 месяца; MV-агрегат «события по часам/фичам» (SummingMergeTree — дашбордные запросы бьют в MV — 40k строк вместо 800M). Результат: p95 дашбордных запросов — 0.4–1.2 с (было 30–90 с), Postgres-реплика разгружена (аналитика ушла целиком), «глубокие» ad-hoc по сырым событиям — 2–8 с; Python-отчёты — clickhouse-connect → polars-фрейм (экспорт тем же конвейером). Извлечённые уроки: первый JOIN двух больших таблиц лёг в память и ушёл в swap — переделали на денормализацию при ETL (плоская факт-таблица + словарь тенантов); одиночные INSERT из обработчика событий дали parts-шторм на стейдже — переведено на батчер в Celery; лимиты на пользователя (память/время) включены после первого «аналитического» запроса, съевшего 90% RAM (учебно). Итог: ClickHouse закрыл нишу «тяжёлая событийная аналитика», Postgres остался OLTP — границы по свойствам движков (ADR зафиксировал выбор против «раздувать Postgres-реплики»).

FAQ

ClickHouse против DuckDB? DuckDB — in-process аналитика (файлы/фреймы, один пользователь, ноль инфраструктуры — обзор); ClickHouse — сервер (много пользователей, потоковая загрузка, персистентность/репликация, HTTP-API); на «аналитик + ноутбука + parquet» — DuckDB проще в разы; на «продуктовые дашборды/телеметрия с потоковой записью» — ClickHouse; они не конкуренты, а разные этажи (DuckDB умеет читать из ClickHouse — комбинированные потоки законны).

Можно ли использовать ClickHouse как основную БД? Нет: OLTP-паттерны (точечные чтения/частые обновления/транзакции/FK-целостность) — мимо его сильных сторон; схема «ClickHouse единственная БД» приводит к изобретению OLTP поверх OLAP (очереди мутаций, самодельная консистентность); правильная пара: Postgres — система записи, ClickHouse — аналитическая копия/событийное хранилище (доставка — CDC/outbox — паттерн).

Почему «too many parts» и как правильно грузить? Каждый INSERT создаёт part; слияния фоновые и не мгновенные — частые мелкие вставки (10 INSERT/с по 1 строке) копят тысячи parts (запросы сканируют метаданные всех — деградация, потом отказ записи); правильно: батчить на клиенте (буфер 10k–100k строк или секунды-минуты — что раньше), async_insert на сервере для неконтролируемых потоков, Buffer-таблицы как прослойка; правило — «вставка реже раза в секунду на таблицу» — ориентир здоровья.

Нужны ли индексы/ключи как в Postgres? Иначе: ORDER BY-ключ — главный инструмент (физическая сортировка — проектировать под фильтры запросов, префиксное правило как у составных индексов); skip-индексы (bloom filter/minmax) — точечно под неключевые фильтры с высокой селективностью (проверять пользу в query_log — бесполезные удалять — они стоят при записи); «создать индексы на всё» — вред (каждый skip-индекс — данные и время вставки); партиционное отсечение по времени — почти всегда главный «индекс» аналитики.

Источники: документация ClickHouse (why fast, MergeTree, CREATE TABLE) — ссылки в шапке.