Прямой ответ
Wide-column хранилища (Cassandra, ScyllaDB, HBase, Bigtable) хранят данные в таблицах с большим числом колонок, где набор колонок может различаться у разных строк. Данные физически упорядочены по ключу, что определяет главное правило проектирования: запрос должен следовать за моделью данных, а не наоборот.
Структура первичного ключа в Cassandra состоит из двух частей:
CREATE TABLE sensor_readings (
device_id uuid,
reading_at timeuuid,
temperature double,
humidity double,
PRIMARY KEY ((device_id), reading_at)
) WITH CLUSTERING ORDER BY (reading_at DESC);
- Партиционный ключ —
(device_id). По нему вычисляется узел и физическое расположение. Все строки с одним партиционным ключом хранятся вместе. - Кластеризационные колонки —
reading_at. Задают порядок строк внутри партиции.
Следствия:
- Запрос обязан включать партиционный ключ. Выборка без него требует перебора всех узлов (
ALLOW FILTERING— признак ошибки проектирования). - Фильтрация возможна только по кластеризационным колонкам и только последовательно. Нельзя отфильтровать по третьей колонке, пропустив вторую.
- Сортировка задаётся при создании таблицы.
CLUSTERING ORDER BYопределяет порядок чтения; изменить его запросом нельзя без полной сортировки.
За хранение отвечает LSM-структура: запись идёт в журнал и memtable, затем выгружается в SSTable и сливается при компакции — подробности в статье про LSM-tree.
Согласованность настраивается на уровне запроса через уровень консистентности: ONE, QUORUM, ALL, LOCAL_QUORUM. При репликации в три копии уровень QUORUM для записи и чтения даёт строгую согласованность в пределах кластера. О теоретических ограничениях — в статье про CAP-теорему.
Практика: проектирование таблиц
1. Одна таблица — один сценарий запроса. В реляционной модели таблицу проектируют под сущность, а запросы строят соединениями. В Cassandra таблицу проектируют под конкретный запрос, допуская дублирование данных в нескольких таблицах.
-- таблица под запрос "последние события устройства"
CREATE TABLE device_events (
device_id uuid, event_at timeuuid, kind text, payload text,
PRIMARY KEY ((device_id), event_at)
) WITH CLUSTERING ORDER BY (event_at DESC);
-- отдельная таблица под запрос "события по типу за период"
CREATE TABLE events_by_kind (
kind text, day date, event_at timeuuid, device_id uuid,
PRIMARY KEY ((kind, day), event_at)
) WITH CLUSTERING ORDER BY (event_at DESC);
2. Следите за размером партиции. Партиция хранится на одном узле и читается целиком. Партиция в сотни мегабайт создаёт горячую точку и замедляет чтение. Практический ориентир — не более десятков мегабайт на партицию.
3. Избегайте горячих партиционных ключей. Если 80 % записи идёт в один ключ, один узел становится узким местом. Распространённый приём — добавление «корзины» в партиционный ключ: (device_id, hour).
4. Дублирование данных — норма. Запись в две-три таблицы ради эффективности чтения — обычный подход. Обратная сторона: приложение обязано писать во все копии, а согласованность между ними обеспечивается на уровне приложения или через лёгкие транзакции.
5. Удаление работает через tombstone. Массовые удаления создают маркеры, которые замедляют чтение до завершения компакции. Для данных со сроком хранения лучше подходит удаление целых партиций или таблиц.
6. Вторичные индексы ограниченно полезны. Локальный вторичный индекс работает в пределах узла и требует обращения ко всем узлам; для высококардинальных данных он неэффективен. Практика — отдельная таблица с нужным порядком ключей.
Рекомендации:
- Начинайте с списка запросов. Выпишите типовые запросы с их фильтрами и сортировкой, затем проектируйте таблицы. Обратный порядок приводит к неработающей схеме.
- Не переносите реляционную схему как есть. Нормализованная схема с соединениями в Cassandra не работает: соединений нет, а
ALLOW FILTERINGскрывает перебор. - Проверяйте план запроса. Отсутствие партиционного ключа в условии видно сразу и должно восприниматься как ошибка.
- Следите за числом частей. Рост количества SSTable на партицию означает, что компакция не успевает; чтение при этом деградирует.
- Планируйте срок хранения. Механизм TTL на уровне таблицы упрощает управление объёмом и позволяет избежать массовых удалений.
Практический сценарий: схема под сущность не выдержала нагрузки
Контекст: логи событий хранили в таблице, спроектированной по образцу реляционной: партиционный ключ — event_id, отдельные таблицы для фильтров по пользователю и по времени.
- Симптом. Запрос «последние 50 событий пользователя» выполнялся 3–6 секунд, хотя объём данных на пользователя был небольшой.
- Диагностика. В условии не было партиционного ключа: запрос использовал
ALLOW FILTERINGи перебирал данные на всех узлах кластера. - Причина. Схему проектировали от сущности «событие», а не от запроса. Идентификатор пользователя оказался обычной колонкой.
- Действия. (а) Создали таблицу
events_by_userс партиционным ключом(user_id)и кластеризацией по времени; (б) заполнили её из исходных данных пачками; (в) приложение начало писать в обе таблицы в одном батче; (г)ALLOW FILTERINGзапретили на уровне код-ревью. - Результат. Время запроса снизилось до 12 мс, нагрузка на кластер упала, потому что исчезли полные переборы.
- Дополнительно. Через месяц обнаружили горячую партицию у сервиса с высоким трафиком; партиционный ключ расширили до
(user_id, hour).
Вывод: в wide-column модели данные организуются под запросы. Попытка сохранить привычную нормализованную схему приводит к запросам, которые формально работают и практически непригодны.
FAQ
Подходит ли Cassandra для данных, которые часто обновляются? Частично. Обновление в Cassandra — это запись новой версии значения, старая исчезает при компакции. Для счётчиков и статусов это работает; для сложной транзакционной логики с частыми изменениями одних и тех же строк лучше подходит реляционная СУБД.
Что выбрать: Cassandra или реляционная база с шардированием? Cassandra даёт горизонтальное масштабирование и доступность без единой точки отказа, но требует перепроектирования модели данных и не поддерживает соединения. Шардированная реляционная база сохраняет SQL и транзакции, но усложняет эксплуатацию — о стратегиях в статье про шардирование.
Можно ли выполнять соединения в Cassandra? Нет. Соединения отсутствуют по проектированию: данные для одного чтения должны лежать в одной партиции. Нужные «соединения» реализуются дублированием данных на этапе записи.
Что такое уровни консистентности и как их выбирать?
Уровень определяет, сколько реплик должны подтвердить операцию. Для критичных данных используют LOCAL_QUORUM и для записи, и для чтения; для телеметрии допустим ONE, что даёт меньшую задержку и более слабые гарантии.
Источники: Apache Cassandra docs (getting started, compaction), Wikipedia (Bigtable) — ссылки в шапке.