Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
ОБЛАСТЬ ПРИМЕНЕНИЯ: NoSQL
В этой статье применяется ряд ключевых понятий Azure Cosmos DB, таких как моделирование данных, секционирование и подготовленная пропускная способность, чтобы продемонстрировать решение реальной практической задачи по подготовке данных.
Если вы регулярно работаете с реляционными базами данных, у вас уже есть представление о моделях данных и определенный набор приемов по их созданию. Azure Cosmos DB имеет ряд уникальных преимуществ и специфичных ограничений. Поэтому многие традиционные рекомендации в этой среде плохо применимы и приводят к созданию неоптимальных решений. В этой статье наглядно демонстрируется полный процесс моделирования данных в Azure Cosmos DB на примере реального использования — от моделирования элементов до размещения сущностей и секционирования контейнеров.
Скачайте или просмотрите созданный сообществом исходный код, иллюстрирующий основные понятия из этой статьи.
Внимание
Участник сообщества внес свой вклад в этот пример кода, и команда Azure Cosmos DB не поддерживает его обслуживание.
Сценарий
В этом упражнении мы рассмотрим домен платформы блогов, где пользователи могут создавать записи. Также они могут добавлять к этим записям отметки "Нравится" и текстовые комментарии.
Совет
Несколько слов, которые здесь выделены курсивом, определяют вид "вещей", с которыми наша модель будет манипулировать.
Давайте добавим к спецификации несколько конкретных требований:
- На главной странице отображается лента недавно созданных записей.
- Мы можем получить все записи пользователя, все комментарии к записи и все отметки "Нравится" к записи.
- Публикации возвращаются с именем пользователя их авторов и с количеством комментариев и отметок "Нравится".
- возвращаются комментарии и отметки "Нравится" вместе с именем пользователя, который их создал
- Когда записи представлены списками, они должны содержать только усеченное резюме своего содержимого.
Описание основных схем доступа
Сначала определим структуру для начальной спецификации, обозначив шаблоны доступа для нашего решения. При разработке модели данных для Azure Cosmos DB важно понимать, какие запросы нашей модели должны служить, чтобы убедиться, что модель эффективно обслуживает эти запросы.
Чтобы упростить общий процесс, мы классифицируем эти различные запросы как команды или запросы, заимствование некоторых словарей из CQRS. В CQRS команды — это запросы на запись (то есть намерения обновить систему), а запросы — запросы только для чтения.
Ниже приведен список запросов, предоставляемых нашей платформой:
- [C1] — создание или изменение пользователя;
- [Q1] — получение сведений о пользователе;
- [C2] — создание или изменение записи;
- [Q2] получение записи
- [Q3] — список записей пользователя в краткой форме;
- [C3] — создание комментария;
- [Q4] — список комментариев к записи;
- [C4] — добавление к записи отметки "Нравится";
- [Q5] — список отметок "Нравится" для записи;
- [Q6] Список x самых свежих созданных записей в краткой форме (лента).
На этом этапе мы не думали о том, что содержит каждая сущность (пользователь, запись и т. д.). Этот шаг обычно является одним из первых, к которым следует обращаться при проектировании для реляционного хранилища. Начнем с этого шага, так как мы должны выяснить, как эти сущности представлены с точки зрения таблиц, столбцов, внешних ключей и т. д. Это значительно менее важно с документной базой данных, которая не требует схемы при записи.
Основная причина, по которой важно определить наши шаблоны доступа с самого начала, заключается в том, что этот список запросов будет нашим набором тестов. Каждый раз, когда мы итерируем модель данных, мы просматриваем каждый из запросов и проверяем его производительность и масштабируемость. Мы вычисляем единицы запросов, потребляемые в каждой модели, и оптимизируем их. Все эти модели используют политику индексирования по умолчанию. Ее можно переопределить путем индексирования конкретных свойств, что может дополнительно уменьшить потребление единиц запросов и задержку.
Версия 1: первая версия
Мы начинаем с двух контейнеров: users
и posts
.
Контейнер пользователей
В этом контейнере хранятся только элементы с данными о пользователях:
{
"id": "<user-id>",
"username": "<username>"
}
Мы разделяем этот контейнер с помощью id
, что означает, что каждый логический раздел в этом контейнере содержит только один элемент.
Контейнер публикаций
В этом контейнере размещаются такие объекты, как записи, комментарии и лайки.
{
"id": "<post-id>",
"type": "post",
"postId": "<post-id>",
"userId": "<post-author-id>",
"title": "<post-title>",
"content": "<post-content>",
"creationDate": "<post-creation-date>"
}
{
"id": "<comment-id>",
"type": "comment",
"postId": "<post-id>",
"userId": "<comment-author-id>",
"content": "<comment-content>",
"creationDate": "<comment-creation-date>"
}
{
"id": "<like-id>",
"type": "like",
"postId": "<post-id>",
"userId": "<liker-id>",
"creationDate": "<like-creation-date>"
}
Мы разделяем этот контейнер с помощью postId
, что означает, что каждая логическая секция в этом контейнере содержит одну запись, все комментарии к этой записи и все лайки к этой записи.
Мы добавили свойство type
в элементы, хранящиеся в этом контейнере, чтобы различать три типа сущностей, размещаемых в этом контейнере.
Кроме того, мы решили использовать ссылки на связанные данные вместо внедрения данных (сравнение этих концепций вы найдете в этом разделе), руководствуясь следующими факторами:
- ограничения на количество сообщений, которые пользователь может создать, не предусмотрено;
- записи могут иметь произвольную длину;
- верхний предел для количества комментариев и (или) отметок "Нравится" для одной записи не предусмотрен;
- требуется возможность добавить к записи комментарий или отметку "Нравится", не обновляя саму запись.
Насколько хорошо работает эта модель?
Пришло время оценить производительность и масштабируемость нашей первой версии. Для каждой из ранее определенных операций мы оценим задержку и количество потребляемых единиц запроса. Это измерение выполняется по фиктивному набору данных 100 000 пользователей, содержащему от 5 до 50 записей от каждого пользователя, а также не более 25 комментариев и 100 отметок "Нравится" для каждой записи.
[C1] — создание или изменение пользователя
Этот запрос реализуется довольно просто: достаточно создать или обновить элемент в контейнере users
. Запросы равномерно распределялись по всем разделам благодаря ключу id
раздела.
Задержка | Плата за RU | Производительность |
---|---|---|
7 мс |
5.71 ЕЗ |
✅ |
[Q1] — получение сведений о пользователе
Получение сведений о пользователе выполняется путем чтения соответствующего элемента из контейнера users
.
Задержка | Плата за RU | Производительность |
---|---|---|
2 мс |
1 ЕЗ |
✅ |
[C2] — создание или изменение записи
Подобно [C1], нам просто нужно записать в контейнер posts
.
Задержка | Плата за RU | Производительность |
---|---|---|
9 мс |
8.76 ЕЗ |
✅ |
[Q2] — восстановление поста
Сначала нужно извлечь соответствующий документ из контейнера posts
. Но этого недостаточно, согласно нашей спецификации, мы также должны агрегировать имя пользователя автора публикации, количество комментариев и количество отметок 'нравится' для публикации. Для перечисленных агрегирований требуется выполнить еще 3 SQL-запроса.
Каждый из отдельных запросов фильтрует по ключу раздела соответствующего контейнера, что именно необходимо для оптимизации производительности и масштабируемости. Но в конечном итоге нам нужно выполнить четыре операции, чтобы вернуть одну запись, и мы улучшим это в следующей версии.
Задержка | Плата за RU | Производительность |
---|---|---|
9 мс |
19.54 ЕЗ |
⚠ |
[Q3] — список записей пользователя в краткой форме
Сначала нам нужно извлечь требуемые записи с помощью запроса SQL, который возвращает записи по определенному пользователю. Но мы также должны выполнять больше запросов, чтобы агрегировать имя пользователя автора и количество комментариев и отметок 'Нравится'.
Представленная реализация имеет несколько недостатков:
- Для каждой записи, возвращенной первым запросом, необходимо выполнить запросы на объединение количества комментариев и лайков.
- Основной запрос не фильтрует по ключу партиции
posts
контейнера, что приводит к значительной нагрузке и сканированию партиций в контейнере.
Задержка | Плата за RU | Производительность |
---|---|---|
130 мс |
619.41 ЕЗ |
⚠ |
[C3] — создание комментария
Комментарий создается путем записи соответствующего элемента в контейнере posts
.
Задержка | Плата за RU | Производительность |
---|---|---|
7 мс |
8.57 ЕЗ |
✅ |
[Q4] — список комментариев к записи
Мы начинаем обработку с запроса, который позволяет извлечь все комментарии к нужной записи. Затем снова нужно получить имена пользователей отдельно для каждого комментария.
Основной запрос позволяет отфильтровать данные контейнера по ключу секции, но раздельный сбор имен пользователей снижает общую производительность. Мы улучшили это позже.
Задержка | Плата за RU | Производительность |
---|---|---|
23 мс |
27.72 ЕЗ |
⚠ |
[C4] — добавление к записи отметки "Нравится"
Так же, как и при выполнении операции [C3], мы создаем нужные элемент в контейнере posts
.
Задержка | Плата за RU | Производительность |
---|---|---|
6 мс |
7.05 ЕЗ |
✅ |
[Q5] — список отметок "Нравится" для записи
Так же, как и в случае с [Q4], мы запрашиваем отметки "Нравится" для нужной записи, а затем собираем имена пользователей.
Задержка | Плата за RU | Производительность |
---|---|---|
59 мс |
58.92 ЕЗ |
⚠ |
[Q6] — список "x" самых свежих записей в краткой форме (веб-канал)
Мы запрашиваем последние записи из контейнера posts
, отсортировав его по убыванию даты создания, а затем собираем имена пользователей и количество комментариев и отметок "Нравится" для каждой из записей.
Снова наш первоначальный запрос не фильтрует по ключу раздела posts
контейнера, что приводит к дорогостоящей рассылке. Это еще хуже, так как мы нацелены на более крупный результирующий набор и сортируем результаты с ORDER BY
оператором, что делает его более дорогим с точки зрения запросных единиц.
Задержка | Плата за RU | Производительность |
---|---|---|
306 мс |
2063.54 ЕЗ |
⚠ |
Размышления о производительности V1
Изучая проблемы с производительностью, которые мы обнаружили в предыдущем разделе, можно выделить два основных класса проблем:
- некоторые запросы требуют выполнения нескольких запросов, чтобы собрать все данные, которые нам нужно вернуть.
- некоторые запросы не фильтруют по ключу раздела контейнеров, что приводит к разветвлению и препятствует нашей масштабируемости.
Давайте займемся устранением каждой из этих проблем, начиная с первой из них.
Версия 2: введение денормализации для оптимизации запросов на чтение
Причина, по которой мы должны выдавать больше запросов в некоторых случаях, заключается в том, что результаты первоначального запроса не содержат все данные, которые нам нужно вернуть. Денормализация данных решает эту проблему в нашем наборе данных при работе с нереляционным хранилищем данных, таким как Azure Cosmos DB.
В нашем примере мы изменим элементы записей, чтобы они содержали имя пользователя, число комментариев и отметок "Нравится":
{
"id": "<post-id>",
"type": "post",
"postId": "<post-id>",
"userId": "<post-author-id>",
"userUsername": "<post-author-username>",
"title": "<post-title>",
"content": "<post-content>",
"commentCount": <count-of-comments>,
"likeCount": <count-of-likes>,
"creationDate": "<post-creation-date>"
}
Мы также изменим элементы комментариев и отметок "Нравится",чтобы они содержали имя пользователя, создавшего их:
{
"id": "<comment-id>",
"type": "comment",
"postId": "<post-id>",
"userId": "<comment-author-id>",
"userUsername": "<comment-author-username>",
"content": "<comment-content>",
"creationDate": "<comment-creation-date>"
}
{
"id": "<like-id>",
"type": "like",
"postId": "<post-id>",
"userId": "<liker-id>",
"userUsername": "<liker-username>",
"creationDate": "<like-creation-date>"
}
Денормализация счетчиков комментариев и отметок "Нравится"
Теперь нам нужно, чтобы при каждом добавлении комментария или отметки "Нравится" увеличивались значения commentCount
или likeCount
для соответствующей записи. Как postId
секционирует наш posts
контейнер, новый элемент (комментарий или лайк) и соответствующая запись находятся в том же логическом разделе. Это позволяет нам использовать хранимую процедуру для выполнения нужной операции.
При создании комментария ([C3]) вместо простого добавления нового элемента в контейнер мы вызываем следующую хранимую процедуру в posts
этом контейнере:
function createComment(postId, comment) {
var collection = getContext().getCollection();
collection.readDocument(
`${collection.getAltLink()}/docs/${postId}`,
function (err, post) {
if (err) throw err;
post.commentCount++;
collection.replaceDocument(
post._self,
post,
function (err) {
if (err) throw err;
comment.postId = postId;
collection.createDocument(
collection.getSelfLink(),
comment
);
}
);
})
}
Эта хранимая процедура принимает в качестве параметров идентификатор записи и текст нового комментария. Она предназначена для выполнения следующих действий:
- извлекает пост
- увеличение значения
commentCount
; - заменяет запись
- добавляет новый комментарий.
Так как хранимые процедуры выполняются как атомарные транзакции, значение commentCount
и фактическое количество комментариев всегда остается в синхронизации.
Разумеется, мы применим аналогичную хранимую процедуру и для добавления новых отметок "Нравится", чтобы увеличивать значение likeCount
.
Денормализация имен пользователей
Для имен пользователей нужен другой подход, так как они располагаются не только в разных секциях, но и в другом контейнере. Для денормализации данных через разделы и контейнеры можно использовать ленту изменений исходного контейнера.
В нашем примере мы настроим канал изменений контейнера users
таким образом, чтобы он реагировал на каждое изменение имен пользователей. Когда это произойдет, мы распространим изменение, вызвав другую хранимую процедуру в контейнере posts
:
function updateUsernames(userId, username) {
var collection = getContext().getCollection();
collection.queryDocuments(
collection.getSelfLink(),
`SELECT * FROM p WHERE p.userId = '${userId}'`,
function (err, results) {
if (err) throw err;
for (var i in results) {
var doc = results[i];
doc.userUsername = username;
collection.upsertDocument(
collection.getSelfLink(),
doc);
}
});
}
Эта хранимая процедура принимает в качестве параметров идентификатор пользователя и его новое имя пользователя. Она предназначена для выполнения следующих задач:
- Извлечение всех элементов, соответствующих условию
userId
(это могут быть записи, комментарии и отметки "Нравится"). - для каждого из этих элементов
- заменяется параметр
userUsername
; - заменяет элемент.
- заменяется параметр
Внимание
Эта операция сопряжена со значительными затратами, так как хранимую процедуру придется выполнить в каждом разделе контейнера posts
. Но мы полагаем, что большинство пользователей выбирают подходящее имя пользователя сразу при регистрации и никогда не изменяют его, а значит, такое обновление будет выполняться очень редко.
Какие преимущества для производительности обеспечила версия 2?
Давайте поговорим о некоторых достижениях производительности версии 2.
[Q2] — восстановление поста
Теперь, когда мы настроили денормализацию, для обработки этого запроса достаточно получить один элемент.
Задержка | Плата за RU | Производительность |
---|---|---|
2 мс |
1 ЕЗ |
✅ |
[Q4] — список комментариев к записи
Снова мы можем избавить от дополнительных запросов, которые извлекали имена пользователей, и в конечном итоге у нас остался только один запрос с фильтрацией по разделительному ключу.
Задержка | Плата за RU | Производительность |
---|---|---|
4 мс |
7.72 ЕЗ |
✅ |
[Q5] — список отметок "Нравится" для записи
Такая же ситуация и при перечислении отметок «Нравится».
Задержка | Плата за RU | Производительность |
---|---|---|
4 мс |
8.92 ЕЗ |
✅ |
Версия 3: обеспечение масштабируемости для всех запросов
Существует еще два запроса, которые мы не полностью оптимизировали в контексте общих улучшений производительности. Эти запросы : [Q3] и [Q6]. Это запросы, которые не фильтруют по ключу раздела целевых контейнеров.
[Q3] — список записей пользователя в краткой форме
Этот запрос уже имеет преимущества от улучшений, представленных в версии 2, что позволяет избежать большего числа запросов.
Но оставшийся запрос по-прежнему не фильтруется по ключу раздела контейнера posts
.
Способ думать об этой ситуации прост:
- Этот запрос должен фильтровать по
userId
, потому что мы хотим получить все записи для конкретного пользователя. - Оно не функционирует хорошо, так как выполняется внутри
posts
контейнера, который не имеетuserId
секционирования. - Очевидно, что мы бы решили нашу проблему производительности, выполнив этот запрос на контейнере, разделенном с помощью
userId
. - Оказывается, что у нас уже есть такой контейнер: контейнер
users
!
Поэтому мы добавим второй уровень денормализации, дублируя все записи в контейнере users
. Таким образом, мы фактически получаем копию наших записей, секционированную только по другому измерению, делая их гораздо более эффективными для извлечения по их userId
.
Теперь users
контейнер содержит два типа элементов:
{
"id": "<user-id>",
"type": "user",
"userId": "<user-id>",
"username": "<username>"
}
{
"id": "<post-id>",
"type": "post",
"postId": "<post-id>",
"userId": "<post-author-id>",
"userUsername": "<post-author-username>",
"title": "<post-title>",
"content": "<post-content>",
"commentCount": <count-of-comments>,
"likeCount": <count-of-likes>,
"creationDate": "<post-creation-date>"
}
В этом примере:
- Мы ввели
type
поле в элементе пользователя, чтобы отличить пользователей от записей, - Мы также добавили
userId
поле в элемент пользователя, которое дублируется сid
полем, но требуется, так какusers
контейнер теперь разделенuserId
(а неid
, как ранее).
Чтобы выполнить эту денормализацию, мы снова применяем фид изменений. Теперь мы реагируем на канал изменений в контейнере posts
, чтобы отправлять в контейнер users
все новые или измененные записи. Поскольку перечисление записей не требует возврата их полного содержимого, в процессе мы можем сократить их.
Теперь наш запрос можно направить к контейнеру users
и использовать фильтрацию по ключу секции этого контейнера.
Задержка | Плата за RU | Производительность |
---|---|---|
4 мс |
6.46 ЕЗ |
✅ |
[Q6] — список "x" самых свежих записей в краткой форме (веб-канал)
Мы должны иметь дело с аналогичной ситуацией здесь: даже после того, как удалось избежать лишних запросов, оставленных ненужными из-за денормализации, введенной в версии 2, оставшийся запрос не фильтрует ключ раздела контейнера.
Применяя тот же подход, производительность и масштабируемость этого запроса можно увеличить, ограничив область его действия одной секцией. Обращение только к одному разделу возможно, потому что нам нужно вернуть лишь ограниченное количество элементов. Чтобы заполнить домашнюю страницу платформы блогов, нам просто нужно получить 100 последних записей без необходимости разбиения на страницы по всему набору данных.
Чтобы оптимизировать этот последний запрос, мы добавляем в архитектуру третий контейнер, полностью посвященный обслуживанию этого запроса. Мы денормализуем наши посты в этот новый контейнер feed
.
{
"id": "<post-id>",
"type": "post",
"postId": "<post-id>",
"userId": "<post-author-id>",
"userUsername": "<post-author-username>",
"title": "<post-title>",
"content": "<post-content>",
"commentCount": <count-of-comments>,
"likeCount": <count-of-likes>,
"creationDate": "<post-creation-date>"
}
Поле type
разделяет этот контейнер, который всегда post
в наших элементах. Это гарантирует, что все элементы в контейнере будут размещены в одном разделе.
Для достижения такой денормализации нужно лишь подключить конвейер канала изменений, который мы создали ранее, для передачи записей в новый контейнер. Здесь важно помнить один важный момент — нам нужно хранить только 100 самых последних записей, иначе размер контейнера может превысить максимальный размер секции. Это ограничение можно реализовать путем вызова пост-триггера при каждом добавлении документа в контейнер.
Вот действие после триггера, который усечает коллекцию:
function truncateFeed() {
const maxDocs = 100;
var context = getContext();
var collection = context.getCollection();
collection.queryDocuments(
collection.getSelfLink(),
"SELECT VALUE COUNT(1) FROM f",
function (err, results) {
if (err) throw err;
processCountResults(results);
});
function processCountResults(results) {
// + 1 because the query didn't count the newly inserted doc
if ((results[0] + 1) > maxDocs) {
var docsToRemove = results[0] + 1 - maxDocs;
collection.queryDocuments(
collection.getSelfLink(),
`SELECT TOP ${docsToRemove} * FROM f ORDER BY f.creationDate`,
function (err, results) {
if (err) throw err;
processDocsToRemove(results, 0);
});
}
}
function processDocsToRemove(results, index) {
var doc = results[index];
if (doc) {
collection.deleteDocument(
doc._self,
function (err) {
if (err) throw err;
processDocsToRemove(results, index + 1);
});
}
}
}
И, наконец, мы переадресуем существующий запрос в новый контейнер feed
:
Задержка | Плата за RU | Производительность |
---|---|---|
9 мс |
16.97 ЕЗ |
✅ |
Заключение
Давайте рассмотрим общие улучшения производительности и масштабируемости, которые мы представили в различных версиях нашего дизайна.
Версия 1 | Версия 2 | Версия 3 | |
---|---|---|---|
[C1] |
7 ms / 5.71 RU |
7 ms / 5.71 RU |
7 ms / 5.71 RU |
[Q1] |
2 ms / 1 RU |
2 ms / 1 RU |
2 ms / 1 RU |
[C2] |
9 ms / 8.76 RU |
9 ms / 8.76 RU |
9 ms / 8.76 RU |
[Q2] |
9 ms / 19.54 RU |
2 ms / 1 RU |
2 ms / 1 RU |
[Q3] |
130 ms / 619.41 RU |
28 ms / 201.54 RU |
4 ms / 6.46 RU |
[C3] |
7 ms / 8.57 RU |
7 ms / 15.27 RU |
7 ms / 15.27 RU |
[Q4] |
23 ms / 27.72 RU |
4 ms / 7.72 RU |
4 ms / 7.72 RU |
[C4] |
6 ms / 7.05 RU |
7 ms / 14.67 RU |
7 ms / 14.67 RU |
[Q5] |
59 ms / 58.92 RU |
4 ms / 8.92 RU |
4 ms / 8.92 RU |
[Q6] |
306 ms / 2063.54 RU |
83 ms / 532.33 RU |
9 ms / 16.97 RU |
Мы оптимизировали сценарий с интенсивным чтением.
Возможно, вы заметили, что мы сосредоточили наши усилия на повышении производительности запросов на чтение (запросы) за счет запросов на запись (команды). При операциях записи теперь часто запускаются действия денормализации через потоки изменений, что делает их более ресурсоемкими и увеличивает время материализации.
Мы оправдываем наше внимание на повышение производительности чтения тем фактом, что блоговая платформа (как и большинство социальных приложений) обладает высокой нагрузкой на чтение. Рабочая нагрузка с большим объемом чтения указывает, что объем запросов на чтение, которые он должен обслуживать, обычно является порядком больше, чем количество запросов на запись. Целесообразно сделать запросы на запись более затратными для выполнения, чтобы запросы на чтение стали дешевле и производительнее.
Если мы рассмотрим самую крайнюю оптимизацию, которую мы сделали, [Q6] сократилось с 2000+ ЕЗ до 17 ЕЗ; мы достигли этого, денормализуя посты с затратами около 10 ЕЗ на элемент. Поскольку мы обслуживаем запросы на получение ленты гораздо чаще, чем создаем или обновляем записи, затраты на денормализацию можно считать несущественными, учитывая общую экономию.
Денормализацию можно применять последовательно
Улучшения масштабируемости, которые мы рассматривали в этой статье, включают денормализацию и дублирование данных по всему набору данных. Важно отметить, что вы не обязаны применить все приемы оптимизации к первому дню работы. Запросы, которые фильтруют по ключам секций, лучше работают при масштабировании, но кросс-секционные запросы могут быть приемлемыми, если они вызываются редко или применяются к ограниченному набору данных. Если вы просто создаете прототип или запускаете продукт с небольшой и контролируемой пользовательской базой, вы можете, вероятно, поэкономить эти улучшения для дальнейшего использования. Важно следить за производительностью вашей модели, чтобы решить, следует ли и когда пришло время их привлечь.
Поток изменений, который мы используем для распространения обновлений в другие контейнеры, сохраняет все эти обновления на постоянной основе. Эта постоянность позволяет запрашивать все обновления, начиная с создания контейнера, и инициализировать денормализованные представления как однократную операцию по актуализации, даже если в вашей системе уже имеются большие объемы данных.
Следующие шаги
После этого введения в практическое моделирование данных и их секционирование, вы можете ознакомиться со следующими статьями, чтобы пересмотреть рассмотренные нами понятия:
- Work with databases, containers, and items (Работа с базами данных, контейнерами и элементами)
- Разделение на разделы в Azure Cosmos DB
- Change feed in Azure Cosmos DB (Канал отслеживания изменений в Azure Cosmos DB)