Схемы Кассандры для начинающих (как и я)

Я был потрясен, когда впервые узнал, что у Кассандры есть столы. Затем последовал еще один вопрос, как вообще это была база данных NoSQL.

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

Я работал над проектом, который широко использует Cassandra в качестве хранилища JSON, и для этого требовалось более глубокое понимание, чем работа с решениями на основе документов, такими как MongoDB или CouchDB, которые уже предоставляют способы хранения JSON из коробки. Первая идея заключалась в хранении большого двоичного объекта JSON в виде строкового значения в одном столбце, но для начала это было довольно плохой идеей, которая сильно противоречит самой причине использования Cassandra, поскольку для этого требовалось, чтобы мое приложение анализировало эту строку JSON каждые время.

Хранение JSON в столбце Cassandra в виде текста или байта [] сильно противоречит самой причине использования Cassandra.

Cassandra больше похожа на базы данных NoSQL на основе ключей и значений, такие как Redis или хеш-таблицу. Для тех, кто пришел из мира реляционного SQL, удобство закончится синтаксисом CQL и установкой первичных ключей. Однако, исходя из NoSQL, как и MongoDB, нужно будет преодолеть язык запросов и схемы, но как только они пройдут, ментальную модель NoSQL можно очень быстро адаптировать к Cassandra.

Это моя лучшая попытка связать дизайн схемы Cassandra с более традиционным форматом данных типа "ключ-значение", таким как JSON, чтобы лучше обучить себя в надежде, что кто-то другой тоже что-то извлечет из этого.

NoSQL! = Без схемы

NoSQL означает Not Only SQL. Это не означает никаких схем. Для многих использование баз данных NoSQL на основе документов и ключ-значение может привести к этому заблуждению. Даже пары данных "ключ-значение", такие как JSON, имеют схему или структуру. Просто она более гибкая к изменениям, чем схема на основе реляционных таблиц.

Единственная разница

Единственное реальное отличие большинства, если не всех, баз данных NoSQL от SQL или реляционных баз данных - это отсутствие связи между двумя отдельными коллекциями данных, таблицами, документами или тем, что каждая база данных использует в качестве термина для определения aa. набор связанных данных. Вы просто запрашиваете таблицу или документ, получаете соответствующие данные, а затем запрашиваете другую таблицу для выполнения кросс-табличного запроса или, а именно операции JOIN в мире SQL (это приведет к аргументу номер 2 ниже).

Единственное отличие баз данных NoSQL от реляционных баз данных на основе SQL - это отсутствие взаимосвязей между коллекциями.

Давний спор между лагерями SQL и NoSQL сводится к следующему:

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

Я не буду говорить о масштабируемости, поскольку большинство пользователей NoSQL должны это знать, и лучше обсудить это где-нибудь в другом месте. Однако я сосредоточусь на базовой структуре Cassandra в надежде на лучшее понимание, которое приведет к созданию более эффективных схем, основанных на запросах.

Кассандра как база данных типа "ключ-значение"

Кассандру можно рассматривать как базу данных «ключ-значение». Под капотом и за пределами языка запросов Cassandra (CQL) и схем он фактически содержит ключ поиска для всех данных в форме первичного ключа.

Учитывая эту таблицу user_tweets:

Поле username действует как первый и единственный первичный ключ, который на языке Кассандры называется ключом раздела. Ключ раздела очень важен в Cassandra, и он в основном группирует все связанные строки вместе для эффективного хранения и поиска. Это станет яснее, когда у нас будет более одного твита на имя пользователя. Таким образом, ключ раздела можно рассматривать как ключ «поиска», аналогичный тому, с чем вы могли иметь дело в любой хэш-таблице, карте, словаре или другой структуре «ключ-значение».

Мы можем упростить его до структуры JSON (не совсем точной, но полезной в качестве ментальной модели для тех, кто пришел из Redis или MongoDB).

Одна вещь, которую следует отметить очень осторожно, заключается в том, что в Cassandra, в отличие от того, что может изобразить массив JSON, каждый раздел сгруппированных строк под ключом раздела хранится несмежно, возможно, на разных узлах, что делает доступ к каждому из них очень дорогостоящим. вместе. В предыдущей таблице, если вы посмотрите на диаграмму отношений "ключ-значение", вы увидите, что каждая строка вообще не связана друг с другом и хранится отдельно друг от друга.

Это невозможно переоценить. Мы никогда не сможем получить доступ к данным второго уровня (например, к электронной почте пользователя) без доступа к первичному ключу имени пользователя. Думайте об этом как о массиве JSON, как было показано ранее. Чтобы получить доступ к моей электронной почте, необходимо заранее указать имя пользователя в качестве ключа.

var tweets = JSON.parse(tweet_data)
var my_email = tweets[0]['jochasinga']['email'];

Примерный эквивалент приведенного выше CQL был бы

SELECT "email" FROM "user_tweets" WHERE "username" = 'jochasinga';

Мы предоставляем первичный ключ или ключ поиска для предложения WHERE, поэтому получить значение email этого имени пользователя очень дешево. .

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

SELECT * FROM "user_tweets" WHERE "email" = '[email protected]';

Вышеупомянутый запрос CQL гласит: «Выбрать все столбцы из таблицы user_tweets, где адресом электронной почты является [email protected]». ”, Возвратит ошибочное предупреждение:

InvalidRequest: code=2200 [Invalid query] message=”Cannot execute this query as it might involve data filtering and thus may have unpredictable performance. If you want to execute this query despite the performance unpredictability, use ALLOW FILTERING

Если задуматься, это имеет смысл. В таблице с большим количеством строк запрос по неключевому столбцу, например email, заставляет Cassandra перебирать каждый первичный ключ username перед тем, как выбрать правильный email value и получение строки. Вот грубое приближение запроса в Javascript, запрашивающего предыдущий JSON:

tweets.forEach(function(elm, i, arr) {
    // Retrieve an array of all the keys
    var keys = Object.keys[elm]
    if elm[keys[0]]['email'] == "[email protected]" {
        return elm
    }
}

Если у вас есть миллион строк твитов или в версии JSON миллион объектов, вы в конечном итоге пройдете через каждый из них, надеясь, что рано или поздно удастся вам удастся. Также помните, что каждый раздел может храниться на отдельном узле друг от друга. Конечно, вам предоставляется возможность выполнить этот запрос в любом случае с помощью флага РАЗРЕШИТЬ ФИЛЬТРАЦИЮ, но вас предупредили.

Все становится яснее, когда мы вводим еще один столбец tweet_id в качестве столбца кластеризации для таблицы твитов. Допустим, я сделал ретвит на твит @banksy, сделав для себя два твита.

tweet_id - это столбец кластеризации с типом time_uuid, упорядочивающий строки под ключом раздела jochasinga в порядке возрастания времени. Поле email объявлено как СТАТИЧЕСКОЕ, что означает, что оно согласовано для всех твитов пользователя и не требует дублирования. Ментальная модель будет похожа на это:

И приблизительное представление в стиле JSON будет примерно таким:

Обратите внимание, что для краткости значение tweet_id упрощено до строки вместо типа time_uuid, а в таблице row_data не было такого поля, как em>. Это просто то, как JSON нужен «ключ» для каждого значения, и то, как tweet_id столбец упорядочивает данные строки в разделе имени пользователя, аналогично тому, как массив хранить данные. Однако в Cassandra tweet_id инкапсулирует информацию о времени, используемую для упорядочивания каждой строки. В этом представлении JSON индекс массива не имеет ничего общего со значением tweet_id.

Теперь, когда «jochasinga» имеет более одного твита, чтобы запросить конкретный твит, вам нужно либо сделать следующее:

  1. Выполните запрос напрямую, используя ключ раздела и уникальный первичный ключ, в данном случае tweet_id.
SELECT * FROM "user_tweets" WHERE "username" = 'jochasinga' AND "id" = bd48ac00-8310-11e5-985d-dd516b67e698;

2. Выполните запрос с использованием ключа раздела и другого уникального столбца первичного ключа ИЛИ неключевого столбца, такого как тело твита.

SELECT * FROM "user_tweets" WHERE "username" = 'jochasinga' AND "tweet" = '@banksy thanks I'll try to check it out!'

А взамен это строка, которую мы получаем

Вторичные индексы

Вторичные индексы - это своего рода «уловка» для превращения неключевого столбца (то есть столбца, который не является первичным ключом) во вторичный «ключ», который вы можете запрашивать так же, как первичный ключ. Это похоже на обратный поиск. Например, запрашивая имя пользователя, вы можете получить адрес электронной почты пользователя.

SELECT "email" FROM "user_tweets" WHERE "username" = 'jochasinga';

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

Примечание. В настоящее время невозможно создать индекс для статического столбца, хотя теоретически это возможно. См. Эту ветку о том, как Apache планирует включить эту функцию в следующие версии.

Мы добавим еще один столбец типа list ‹text› для хранения хэштегов в твите. Имеет смысл рассмотреть, как Twitter отображает твиты с соответствующим хэштегом в результатах поиска. Например, когда пользователь ищет все твиты с хэштегом #funnycats, Twitter мог запросить его следующим образом:

SELECT * FROM "user_tweets" WHERE "hashtags" CONTAINS "funnycats";

Довольно информативный.

Но без «пометки» столбца hashtags он вернет жалобу так же, как мы пытались выполнить запрос с неключевым значением столбца.

Давайте добавим столбец хэштегов и посмотрим, что из этого получится.

ALTER TABLE "user_tweets" ADD "hashtags" list<text>;

На этом этапе нам нужно будет изменить данные столбца hashtags предыдущего твита, поскольку столбец, который мы только что добавили, не содержит ничего в каждой строке.

UPDATE "user_tweets" SET "hashtags" = ['art', 'graffiti', 'nyc'] WHERE "username" = 'jochasinga' AND "id" = bd48ac00-8310-11e5-985d-dd516b67e698;
UPDATE "user_tweets" SET "hashtags" = ['rad'] WHERE "username" = 'banksy' AND "id" = 76e7a4d0-e796-11e3-90ce-5f98e903bf02;
UPDATE "user_tweets" SET "hashtags" = ['sad', 'HopeForUkraine'] WHERE "username" = 'random_may' AND "id" = fa507380-8310-11e5-985d-dd516b67e698;

Затем создайте вторичный индекс в столбце хэштеги.

CREATE INDEX ON "user_tweets" ("hashtags")

Мы появимся с новой версией user_tweets, которая выглядит так:

Теперь мы сможем выполнять запросы по хэштегам, как это сделал бы поиск в Twitter.

SELECT * FROM "user_tweets" WHERE "hashtags" CONTAINS 'art';

Угадайте, какая строка будет возвращена. (мой первый твит, конечно!)

Я сказал, что вторичные индексы - это хитрость, потому что они плохо масштабируются и их следует использовать с осторожностью, особенно в столбцах с высокой мощностью, что означает, что данные в основном различаются. Он больше подходит для столбцов с низкой мощностью, например, для статических столбцов (прокрутите вверху, чтобы узнать, почему пока невозможно создать индекс для статических столбцов). Эта статья отлично подходит для проработки проблемы, связанной с вторичными индексами Кассандры.

Заключение

Лучше думать о Cassandra как о структуре отсортированных пар документов «ключ-значение», а не ячеек данных, и это оказалось очень полезным для моего процесса проектирования схемы вокруг того, что мне было нужно. Ключи разделения и столбцы кластеризации - почти единственные ваши самые важные друзья.

НЕ ОГРАНИЧИВАЙТЕ СЕБЯ СХЕМЫ.

Вот несколько правил, которые вы можете запомнить

  • Разделить - значит отделить две вещи друг от друга. Итак, если вы создадите свою таблицу следующим образом
CREATE TABLE "my_users" (
    id uuid,
    email text,
    password blob,
    country text,
    PRIMARY KEY (country, id)
);

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

CREATE TABLE "my_users" (
    id uuid,
    email text,
    password blob,
    city text,
    country text,
    PRIMARY KEY ((country, city), id)
);

Пользователи из одной страны, но из разных городов будут храниться отдельно. Строки в разделе будут отсортированы по идентификатору.

  • Столбец кластеризации или второй первичный ключ решают, как вы хотите упорядочить базу данных. (По возрастанию или по убыванию)
  • Вторичные индексы избегают денормализации или процесса создания избыточных данных для создания эффективных однораздельных чтений, но они довольно дороги и должны использоваться для запросов со средним трафиком.
  • Добавляйте столбцы по желанию. Cassandra гибкая, и строки без данных не занимают память (нет значения указателя NULL для отсутствующего значения).