Два разных «колоночных»
Термин «колоночная БД» путает, потому что обозначает две разные вещи. Wide-column (Cassandra, HBase) — это NoSQL для огромной записи и запросов по ключу. Column-oriented / columnar (ClickHouse, Redshift, Parquet) — это аналитические (OLAP) хранилища, где данные физически лежат по колонкам.
Их объединяет слово «колонка», но задачи противоположны: одно — про распределённую транзакционную запись, другое — про сканирующую аналитику. На собеседовании важно их не смешивать.
Wide-column: модель Cassandra
В Cassandra данные раскладывают строго под запросы. Первичный ключ состоит из partition key (определяет узел/партицию) и clustering columns (определяют порядок строк внутри партиции).
Запросы эффективны только по этому ключу: «дайте сообщения чата X по времени» прекрасно, а «найдите по произвольному полю» — нет. Поэтому в Cassandra нормально дублировать данные в несколько таблиц, по одной на паттерн запроса (query-first modeling).
-- партиция = chat_id, внутри упорядочено по времени
CREATE TABLE messages (
chat_id uuid,
sent_at timestamp,
author text,
body text,
PRIMARY KEY ((chat_id), sent_at)
) WITH CLUSTERING ORDER BY (sent_at DESC);
Write path: LSM-дерево
Cassandra оптимизирована под запись благодаря LSM-tree (log-structured merge). Запись идёт в commit log и в memtable (в памяти), а затем сбрасывается на диск неизменяемыми SSTable. Это превращает случайную запись в последовательную — отсюда высокий throughput.
Цена — чтение может задеть несколько SSTable, а фоновый процесс compaction периодически сливает их. Удаления реализуются tombstone-маркерами, обилие которых может замедлять чтение.
Ошибки моделирования в Cassandra
Почти все проблемы с Cassandra — это последствия неудачно выбранного partition key. Три классических симптома стоит знать наизусть.
Слишком широкая партиция: ключ выбран так, что в одну партицию попадают миллионы строк (например, партиция по дате без разбиения). Партиция целиком живёт на одном узле, и чтение с записью упираются в него, а compaction становится дорогим. Лечится добавлением в partition key ещё одного измерения — bucket'а (дата + номер корзины).
Горячая партиция: ключ формально узкий, но обращения сосредоточены в одной точке (все события одного крупного клиента). Проблема та же — перекос нагрузки на узел, а не на кластер.
И запрос не по ключу. Cassandra честно отказывается выполнять такой запрос, а подсказка ALLOW FILTERING заставляет её просканировать весь кластер — на проде это способ уронить его целиком. Вторичные индексы (secondary index) выглядят спасением, но работают плохо на высокой кардинальности и на больших кластерах, потому что запрос всё равно идёт на все узлы. Штатный путь — не индекс, а ещё одна таблица под этот паттерн запроса.
Отсюда и главный сдвиг мышления: дублирование данных здесь не ошибка нормализации, а осознанный приём. Запись дёшева, дисковое место дёшево, а вот запрос не по ключу невозможен — поэтому одну и ту же сущность спокойно пишут в несколько таблиц, по одной на сценарий чтения.
-- та же сущность во второй таблице — под другой паттерн чтения
CREATE TABLE messages_by_author (
author text,
sent_at timestamp,
chat_id uuid,
body text,
PRIMARY KEY ((author), sent_at)
) WITH CLUSTERING ORDER BY (sent_at DESC);
-- широкая партиция разбивается bucket'ом, чтобы не жить на одном узле
CREATE TABLE events_by_day (
day date,
bucket int, -- 0..N: разносим нагрузку по узлам
ts timestamp,
payload text,
PRIMARY KEY ((day, bucket), ts)
);
Column-oriented OLAP
Аналитические колоночные БД хранят каждую колонку отдельно. Для запросов вида «sum(amount) по миллиарду строк за месяц» это идеально: читаются только нужные колонки, а не целые строки.
Однородность колонки даёт сильное сжатие (RLE, dictionary, delta) и векторизованную обработку. Платят за это медленными точечными вставками/обновлениями — поэтому OLAP-стор не годится для транзакционной нагрузки (OLTP).
OLTP против OLAP
OLTP (строковые БД, Postgres/Cassandra) — много мелких операций над отдельными записями. OLAP (колоночные, ClickHouse) — тяжёлые сканирующие агрегаты по огромным наборам.
Типовая архитектура: транзакции живут в OLTP, а аналитику периодически выгружают в OLAP через ETL/CDC. Пытаться делать тяжёлую аналитику прямо на боевой OLTP-базе — частая причина деградации.
Как отвечать на собеседовании
Сильный ответ: сразу разведите wide-column (query-first, partition+clustering keys, LSM, запись) и columnar OLAP (хранение по колонкам, сжатие, сканирующая аналитика), и привяжите выбор к OLTP/OLAP-профилю нагрузки.
Начните именно с этого разведения — оно само по себе показывает уровень, потому что «колоночными» называют две системы с противоположными задачами. Cassandra — про распределённую запись и чтение по ключу, ClickHouse — про сканирующую аналитику.
Про Cassandra объясняйте ключ по частям: partition key определяет, на каком узле лежат данные, clustering columns — порядок строк внутри партиции. Из этого выводится всё остальное: запросы эффективны только по ключу, поэтому моделируют от запросов и спокойно дублируют данные в несколько таблиц.
Назовите LSM-дерево и главное следствие: запись идёт в commit log и memtable, затем сбрасывается неизменяемыми SSTable — случайная запись превращается в последовательную, отсюда высокий throughput. И честно назовите цену: чтение задевает несколько SSTable, нужен compaction, а tombstone'ы от удалений замедляют чтение.
Про OLAP отвечайте механизмом: колонки лежат отдельно, поэтому агрегат по одной колонке не читает остальные, а однородные данные в колонке сжимаются в разы (RLE, dictionary, delta) и обрабатываются векторизованно. Цена — дорогие точечные вставки и обновления, то есть OLTP-нагрузка противопоказана.
Завершите архитектурным выводом: транзакции живут в OLTP-базе, аналитику выгружают в OLAP через ETL или CDC, а тяжёлые отчёты прямо на боевой базе — типичная причина деградации прода.