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 Erick Ramirez edited

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

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

Сейчас мы работаем в 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

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 Erick Ramirez edited
В случае с касссандрой (я с ней почти незнаком)

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

Очень советую начать с этого простого курса: 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

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 ·