Bringing together the Apache Cassandra experts from the community and DataStax.

Want to learn? Have a question? Want to share your expertise? You are in the right place!

Not sure where to begin? Getting Started

 

question

sash avatar image
sash asked ·

Модель данных

Всем привет, я понимаю, что вопрос с моделью данных довольно специфичен и чтобы правильно ответить на него, нужно глубоко погрузится в задачу, но я все же постараюсь описать нашу задачу.

Сейчас мы работаем в MongoDB, поэтому возможны какие то отсылки.

У нас структуре проекта есть тенанты - это изолированная единица агрегации. В ней есть большое кол-во пользователей (до 2 миллионов). Каждый пользователь может взаимодействовать внутри тенанта с каким то количеством "ботов" по сути это набор скриптов в виде шагов бота. Каждый шаг это по сути переменная, мы фиксируем когда он пройдет, кроме времени в переменную могут быть записаны любые другие данные. Все усложняется тем, что каждый бот это непредсказуемое сочетание шагов и переменных. То есть боты могут быть абсолютно разными в рамках тенанта. И нам надо делать выборки по статусам и состояниям. На пользовательском уровне это что то на подобии фильтров. Чтобы например отсортировать всех клиентов прошедших бота 1 и бота два и остановившихся на боте 3 на шаге 5 со значением перемененной Возраст => 35. В случае с SQL придется растаскивать сущности по таблицам и потом при запросах их джойнить, что неприемлемо медленно. В случае с Монгой все решается индексами, но в структуре Json клиента появляется огромная портянка истории, котрорую надо как то обслуживать.

В случае с касссандрой (я с ней почти незнаком) мы должны иметь одну таблицу на тенант для фиксации вышеуказанных статусов и значений. Скажем в строках будут пользователи, а столбцы будут представлять из себя структуру "бота". Если в боте 100 шагов и 50 переменных, а в другом боте 200 шагов и 500 ременных то на одного и того же пользователя мы получим 2 строки в одной будет 150 столбцов а в другой 700, при этом ключ будет состоять из ID пользователя и ID бота? и в такой таблице всегда можно отсортировать строки по любому из столбцов, примерно так как это делается в фильтрах задач в Jira. https://www.awesomescreenshot.com/video/3490931?key=1c0eb6611d1e98baeab7e419f5eb627d

Или же если ключ это просто ИД пользователя, то в строку к боту добавлять супер столбцы, структура каждого представляла бы структуру шагов и сущностей бота?

Поругайте? возможно я вообще неправильно понял концепцию кассандры.

data modeling
10 |1000 characters needed characters left characters exceeded

Up to 8 attachments (including images) can be used with a maximum of 1.0 MiB each and 10.0 MiB total.

1 Answer

Artem Chebotko avatar image
Artem Chebotko answered ·
В случае с касссандрой (я с ней почти незнаком)

Скажу сразу что проблема решаема в Кассандре, но нужно потратить время чтобы разобраться как Кассандра хранит данные и какие запросы возможны.

Очень советую начать с этого простого курса: https://www.datastax.com/learn/cassandra-fundamentals. Как минимум, посмотрите уроки 4, 5, 6 и 8. Это чтобы начать разбираться что в таблице может быть 3 ключа - primary key, partition key и clustering key - и как они влияют на запросы.


Дальше я отвечу на некоторые ваши вопросы и затем приведу пример.

мы должны иметь одну таблицу на тенант для фиксации вышеуказанных статусов и значений.

Это один из способов и у него есть ограничения: если тенантов несколько сотен, то лучшего его не использовать. Таблиц на одном Кассандра кластере лучше не более 500, а то и меньше (250). Другой способ который я предпочитаю - это хранить данные всех тенантов в одной таблице но в разных разделах. Именно его я использую в примере ниже.


Скажем в строках будут пользователи, а столбцы будут представлять из себя структуру "бота". Если в боте 100 шагов и 50 переменных, а в другом боте 200 шагов и 500 ременных то на одного и того же пользователя мы получим 2 строки в одной будет 150 столбцов а в другой 700, при этом ключ будет состоять из ID пользователя и ID бота?

Нет, не так. У каждого пользователя будет свой раздел (partition) в таблице. Количество столбцов будет одинаково, а вот количество строк в каждом разделе будет разным. Опять же смотрите пример ниже.


и в такой таблице всегда можно отсортировать строки по любому из столбцов, примерно так как это делается в фильтрах задач в Jira.

Это сложнее. Кассандра не сортирует данные. Она хранит данные в отсортированном виде по ключу кластеризации (clustering key - смотрите урок 5). Чтобы поддерживать разные критерии сортировки с помощью Кассандра, нужно создавать дополнительные таблицы с соответствующими clustering keys и сохранять туда копии данных. Это один способ. Другой способ - сортировать данные в приложении. Небольшое количество строк может быть проще отсортировать в приложении.


Или же если ключ это просто ИД пользователя, то в строку к боту добавлять супер столбцы, структура каждого представляла бы структуру шагов и сущностей бота?

Супер столбцы (super column family) - использовались в очень старой версии Кассандры лет 8-10 назад. Вам это не нужно.


ПРИМЕР

Из описания, данные имеют приблизительно следующую организацию:

тенант -> пользователь -> бот -> шаг -> переменная -> значение

или

tenant -> user -> bot -> step -> parameter -> value 


Вот пример таблицы с которой можно начать.

Но это только начало. Для разных запросов возможно прийдется создавать дополнительные таблицы и индексы. Где-то может даже фильтровать в приложении если данных немного.

CREATE TABLE data_by_tenant_user (
    tenant     TEXT,
    user       TEXT,
    bot        TEXT,
    step       TEXT,
    parameter  TEXT,
    value      TEXT,
    PRIMARY KEY( (tenant, user), bot, step, parameter)
);
-- Partition key:  tenant, user
-- Clustering key: bot, step, parameter

INSERT INTO data_by_tenant_user (tenant, user, bot, step, parameter, value) 
VALUES ('TA','user1','botX','step1','status','completed');
INSERT INTO data_by_tenant_user (tenant, user, bot, step, parameter, value) 
VALUES ('TA','user1','botX','step1','timestamp','2021-04-26 10:33:55');
INSERT INTO data_by_tenant_user (tenant, user, bot, step, parameter, value) 
VALUES ('TA','user1','botX','step2','status','in progress');
INSERT INTO data_by_tenant_user (tenant, user, bot, step, parameter, value) 
VALUES ('TA','user1','botY','step1','satisfaction','high');
INSERT INTO data_by_tenant_user (tenant, user, bot, step, parameter, value) 
VALUES ('TB','user991','botZ','step1','status','completed');
INSERT INTO data_by_tenant_user (tenant, user, bot, step, parameter, value) 
VALUES ('TB','user991','botZ','step1','date','2020-11-23');
INSERT INTO data_by_tenant_user (tenant, user, bot, step, parameter, value) 
VALUES ('TB','user991','botZ','step2','temperature','33C');


SELECT * FROM data_by_tenant_user;


 tenant | user    | bot  | step  | parameter    | value
--------+---------+------+-------+--------------+---------------------
     TB | user991 | botZ | step1 |         date |          2020-11-23
     TB | user991 | botZ | step1 |       status |           completed
     TB | user991 | botZ | step2 |  temperature |                 33C
     TA |   user1 | botX | step1 |       status |           completed
     TA |   user1 | botX | step1 |    timestamp | 2021-04-26 10:33:55
     TA |   user1 | botX | step2 |       status |         in progress
     TA |   user1 | botY | step1 | satisfaction |                high

SELECT * FROM data_by_tenant_user WHERE tenant = 'TA' AND user = 'user1';


 tenant | user  | bot  | step  | parameter    | value
--------+-------+------+-------+--------------+---------------------
     TA | user1 | botX | step1 |       status |           completed
     TA | user1 | botX | step1 |    timestamp | 2021-04-26 10:33:55
     TA | user1 | botX | step2 |       status |         in progress
     TA | user1 | botY | step1 | satisfaction |                high
4 comments Share
10 |1000 characters needed characters left characters exceeded

Up to 8 attachments (including images) can be used with a maximum of 1.0 MiB each and 10.0 MiB total.

Артем спасибо огромное, на примере все намного понятнее. Про суперстолбцы видимо получил звонок из прошлого. Да, тенантов много и их даже не сотни, их 10ки тысяч. В каждом тенанте 10ки ботов с сотнями степов и 10ками переменных и сотни тысяч юзеров. Поэтому действительно много таблиц - не вариант.

Это то что нужно:

SELECT * FROM data_by_tenant_user WHERE tenant = 'TA' AND bot = 'botX', step= "step3-200", value = "in progress";

Вроде все норм но изза value="in progress" запрос вынужден будет делать фулскан, что не норма. (Поправте если ошибаюсь).

Скажем кейс такой, есть канбан доска на которой надо показывать пользователей находящихся на том или ином шаге бота или с тем или иным значением Value.

[Converted answer to comment]

0 Likes 0 ·

К сожалению, оба SELECT запроса в вашем посте работать не будут. В тех уроках, которые я посоветовал выше, можно будет их попробовать в терминале прямо в браузере. Ничего устанавливать даже не нужно.


Вроде все норм но изза value="in progress" запрос вынужден будет делать фулскан, что не норма. (Поправте если ошибаюсь).

Не ошибаетесь. Фулскан - это очень плохо.


При записи, чтобы не забыть что у нас 2 таблицы, мы используем Batch.
Так?

Логично, но нет. В этом случае BATCH лучше не использовать. Просто обычные INSERTs.

BATCH рекомендуется использовать если он работает только с одним разделом (partition). Поскольку у нас разные таблицы, то и разделы разные.


Например в общем списке все пользователи у которых температура = 33с и находятся на шаге 1 бота Z?

Чтобы найти пользователей по значению, переменной, шагу и боту, нужна новая таблица. Вот как оне будет работать - смотрите в users_by_value.txt

0 Likes 0 ·
users-by-value.txt (1.7 KiB)

Получается нужно создать отдельную таблицу которая будет повторять по своей структуре первую (возможно шаг бота нам ненужен) но с другим набором столбцов в PRIMARY KEY:

CREATE TABLE data_by_tenant_user_values (
  tenant TEXT,
  user TEXT,
  bot TEXT,
  parameter TEXT,
  value TEXT,
  PRIMARY KEY( (tenant, user),bot,parameter,values)
);

Таким образом запрос будет выглядеть вот так:

SELECT * FROM data_by_tenant_user_values WHERE tenant = 'TA' AND value => "33C";

и в ответ получим

 tenant |    user |  bot | parameter   | value
--------+---------+------+-------------+--------------
     TB | user991 | botZ | temperature | 33C

Джойнов нет, поэтому возвращаясь к примеру с канбан доской в одном столбике назовем его "шаг 1 ботаZ" сделаем запрос по таблице data_by_tenant_user а получим всех юзеров на шаге 1 ботаZ, а в столбике "Переменная - температура до 33" сделаем запрос по data_by_tenant_user_values и получим одну запись.

0 Likes 0 ·

Так?

Если я правильно понял главную мысль то это здорово и это неплохо ложится на "столбики" в канбане, где реально можно использовать разные запросы, так же такой подход неплохо будет работать если мне надо оперировать фильтрами по переменным отдельно и по шагам в ботах отдельно. Но если мне нужен составной фильтр? Например в общем списке все пользователи у которых температура = 33с и находятся на шаге 1 бота Z?

Кстати насчет сортировки, главное применение у нас это: фильтры, то есть почти всегда получить какой то список Юзеров, чтобы потом массивом его куда то загнать или где то отобразить, поэтому сортировку да, возможно будет лучше устроить на уровне приложения с пагинациями итд. На уровне запроса она и не нужна, главное это отфильтрованный список.

Спасибо за ваше время Артем, вы делаете отличную работу.

0 Likes 0 ·