С Cassandra чаще всего ошибаются не в эксплуатации, а в моделировании: проектируют таблицы, как в реляционной базе, а потом упираются в то, что нужный запрос невозможен. Модель данных Cassandra подчинена её архитектуре: данные распределены по узлам, JOIN нет, и это меняет сам порядок проектирования. Разберём его.
Wide-column: строки с гибким набором колонок
Внешне таблица Cassandra похожа на реляционную — строки и колонки, есть язык запросов CQL с SQL-образным синтаксисом. Но устройство иное: физически данные хранятся как широкие строки — по ключу раздела лежит группа записей, отсортированных и сгруппированных. Отсюда название «wide-column»: одна логическая партиция может содержать от одной до миллионов упорядоченных строк.
Это не хранилище документов, как MongoDB, и не таблицы со связями, как PostgreSQL. Ближе всего аналогия: распределённая структура, где по ключу раздела лежит отсортированная таблица записей. Всё проектирование крутится вокруг того, как задать этот ключ.
Первичный ключ: две разные роли
Главное, что нужно усвоить: в Cassandra первичный ключ состоит из двух частей с разными задачами.
CREATE TABLE messages (
chat_id uuid, -- partition key: где лежат данные
sent_at timestamp, -- clustering column: порядок внутри партиции
message_id uuid,
author text,
body text,
PRIMARY KEY ((chat_id), sent_at, message_id)
);
Ключ раздела (partition key) — первая часть (здесь chat_id, в двойных скобках). Он определяет на каком узле лежат данные: его хеш даёт токен, а токен — узел (см. архитектуру). Все строки с одним chat_id физически лежат вместе, в одной партиции, на одних и тех же репликах.
Колонки кластеризации (clustering columns) — остальные части (sent_at, message_id). Они задают порядок строк внутри партиции. Здесь сообщения одного чата хранятся отсортированными по времени — и читать их по порядку можно эффективно, без сортировки на лету.
Разделение ролей — суть модели: partition key раскидывает данные по кластеру ради масштаба, clustering columns упорядочивают их внутри партиции ради быстрых диапазонных чтений.
Что можно и нельзя в запросе
Из устройства ключа вытекают жёсткие правила запросов, которые новичка удивляют:
- запрос должен указывать ключ раздела. Без него Cassandra не знает, на каком узле искать, и пришлось бы обойти весь кластер. Поэтому
WHERE chat_id = ?— обязателен; - по колонкам кластеризации можно фильтровать и брать диапазоны, но только слева направо, в порядке их объявления (нельзя фильтровать по второй, пропустив первую);
- по обычным колонкам фильтровать нельзя (
WHERE body = ?не сработает). Можно заставить черезALLOW FILTERING, но это обход всего кластера — на проде так почти никогда не делают; - нет
JOIN, нетGROUP BYс произвольной агрегацией, нет подзапросов. Модель рассчитана на быстрый доступ по ключу, а не на аналитику.
-- Быстро: указан ключ раздела + диапазон по времени
SELECT * FROM messages
WHERE chat_id = ? AND sent_at > ?;
-- Плохо: нет ключа раздела — обход всего кластера
SELECT * FROM messages WHERE author = ?; -- потребует ALLOW FILTERING
Проектирование от запросов
Отсюда главный принцип, переворачивающий привычку реляционщика: в Cassandra проектируют не от данных, а от запросов. Порядок такой:
- выпишите все запросы, которые сервис будет делать в продакшене;
- под каждый запрос спроектируйте свою таблицу так, чтобы запрос бил по ключу раздела;
- если один и тот же факт нужен в нескольких запросах под разными углами — храните его в нескольких таблицах.
То есть одни и те же данные сознательно дублируют — это денормализация, и в Cassandra она норма, а не грех. Диск дёшев, а вот запрос без ключа раздела — дорог. Согласованность копий между таблицами — забота приложения (часто через запись сразу в несколько таблиц одним пакетом).
Практический вывод: если заранее не известно, как будут спрашивать данные, или запросы разнообразны и часто меняются, — Cassandra противопоказана. Она вознаграждает предсказуемые обращения по ключу и наказывает за произвольную аналитику.
Грабли размера партиции
Ещё одна частая ошибка — неудачный выбор ключа раздела, из-за которого партиция разрастается. Две крайности:
- слишком «горячий» ключ — если по одному значению ключа скапливается непропорционально много данных или запросов (например, ключ раздела — страна, и вся Россия в одной партиции), эта партиция становится узким местом: она целиком лежит на одних репликах, и они перегружены;
- безграничный рост — если в партицию бесконечно дописываются строки (ключ раздела —
sensor_id, а показания идут годами), она пухнет до неуправляемого размера.
Лечится это составным ключом раздела с «корзиной»: например, ((sensor_id, day), reading_ts) — тогда данные каждого дня образуют отдельную партицию разумного размера. Проектирование ключа раздела — не мелочь, а главное решение при работе с Cassandra.
Коротко
- Физически Cassandra хранит широкие строки: по ключу раздела лежит группа отсортированных записей. Это не документы и не реляционные таблицы со связями.
- Первичный ключ = partition key (определяет узел, распределяет данные) + clustering columns (задают порядок внутри партиции).
- Запрос обязан указывать ключ раздела; по обычным колонкам фильтровать нельзя (
ALLOW FILTERING— обход кластера, не для прода); нетJOINи произвольной агрегации. - Проектируют от запросов, сознательно денормализуя данные в несколько таблиц; согласованность копий — на приложении.
- Ключ раздела выбирают так, чтобы партиции не были ни «горячими», ни безгранично растущими (приём «корзины» в составном ключе).
Что почитать дальше
- Согласованность и репликация — какие гарантии даёт запись и чтение поверх этой модели.
- Архитектура Cassandra — почему запрос обязан указывать ключ раздела и как данные распределены по кольцу.
- Cassandra, PostgreSQL или MongoDB — сравнение моделей данных трёх баз.