Процесс моделирования и секционирования данных в Azure Cosmos DB на примере реального использования

ПРИМЕНИМО К: API SQL

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

Если вы регулярно работаете с реляционными базами данных, у вас уже есть представление о моделях данных и определенный набор приемов по их созданию. Azure Cosmos DB имеет ряд уникальных преимуществ и специфичных ограничений. Поэтому многие традиционные рекомендации в этой среде плохо применимы и приводят к созданию неоптимальных решений. В этой статье наглядно демонстрируется полный процесс моделирования данных в Azure Cosmos DB на примере реального использования — от моделирования элементов до размещения сущностей и секционирования контейнеров.

Скачайте или просмотрите созданный сообществом исходный код, иллюстрирующий основные понятия из этой статьи. Этот пример кода был создан участником сообщества. Команда Azure Cosmos DB не отвечает за его обслуживание.

Сценарий

В этом упражнении мы рассмотрим платформу для ведения блогов, на которой пользователи могут публиковать записи. Также они могут добавлять к этим записям отметки "Нравится" и текстовые комментарии.

Совет

Несколько слов, которые здесь выделены курсивом, определяют характер сущностей и концепций, с которыми будет работать наша модель.

Давайте добавим к спецификации несколько конкретных требований:

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

Описание основных схем доступа

Сначала определим структуру для начальной спецификации, обозначив шаблоны доступа для нашего решения. При разработке модели данных для Azure Cosmos DB важно понять, какие операции будет обслуживать эта модель, чтобы повысить эффективность обработки этих запросов.

Чтобы упростить общий процесс, мы разделяем возможные операции на команды и запросы, используя термины из концепции CQRS. Командами считаются операции записи (то есть с целью обновления информационной системы), а запросами — обращения только для чтения.

Вот список операций, которые можно будет выполнять на нашей платформе:

  • [C1]  — создание или изменение пользователя;
  • [Q1]  — получение сведений о пользователе;
  • [C2]  — создание или изменение записи;
  • [Q2]  — получение записи;
  • [Q3]  — список записей пользователя в краткой форме;
  • [C3]  — создание комментария;
  • [Q4]  — список комментариев к записи;
  • [C4]  — добавление к записи отметки "Нравится";
  • [Q5]  — список отметок "Нравится" для записи;
  • [Q6]  — список x самых свежих записей в краткой форме (веб-канал).

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

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

Версия 1: первая версия

Первыми объектами у нас будут два контейнера: users и posts.

Контейнер users

В этом контейнере хранятся только элементы с данными о пользователях:

{
    "id": "<user-id>",
    "username": "<username>"
}

Для него мы выполним секционирование по id. То есть каждая логическая секция в этом контейнере будет содержать только один элемент.

Контейнер posts

В этом контейнере размещаются записи, комментарии и отметки "Нравится":

{
    "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.

Запись одного элемента в контейнер users

Задержка Стоимость в ЕЗ Производительность
7 мс 5,71 ЕЗ

[Q1] — получение сведений о пользователе

Получение сведений о пользователе выполняется путем чтения соответствующего элемента из контейнера users.

Получение одного элемента из контейнера users

Задержка Стоимость в ЕЗ Производительность
2 мс 1 ЕЗ

[C2] — создание или изменение записи

Аналогично операции [C1], выполняется путем записи в контейнер posts.

Запись одного элемента в контейнер posts

Задержка Стоимость в ЕЗ Производительность
9 мс 8,76 ЕЗ

[Q2] — получение записи

Сначала нужно извлечь соответствующий документ из контейнера posts. Но этого недостаточно, ведь согласно спецификации требуется предоставить имя пользователя автора записи, а также количество комментариев и отметок "Нравится" для этой записи. Для этого мы выполним еще три запроса SQL.

Получение записи и дополнительных статистических данных

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

Задержка Стоимость в ЕЗ Производительность
9 мс 19,54 ЕЗ

[Q3] — список записей пользователя в краткой форме

Сначала нам нужно извлечь требуемые записи с помощью запроса SQL, который возвращает записи по определенному пользователю. Также мы должны выполнить дополнительные запросы для получения имени пользователя автора, количества комментариев и отметок "Нравится".

Получение всех записей пользователя и статистическая обработка дополнительных данных

Представленная реализация имеет несколько недостатков:

  • сбор данных о количестве комментариев и отметок "Нравится" выполняется отдельно для каждой записи, которая получена в результатах первого запроса;
  • основной запрос не использует фильтр по ключу раздела в контейнере posts, что приводит к размножению запросов и сканированию по всем разделам в контейнере.
Задержка Стоимость в ЕЗ Производительность
130 мс 619,41 ЕЗ

[C3] — создание комментария

Комментарий создается путем сохранения соответствующего элемента в контейнер posts.

Запись одного элемента в контейнер posts

Задержка Стоимость в ЕЗ Производительность
7 мс 8,57 ЕЗ

[Q4] — список комментариев к записи

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

Получение всех комментариев к записи и статистическая обработка дополнительных данных

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

Задержка Стоимость в ЕЗ Производительность
23 мс 27,72 ЕЗ

[C4] — добавление к записи отметки "Нравится"

Так же, как и при выполнении операции [C3], мы создаем нужные элемент в контейнере posts.

Запись одного элемента в контейнер posts

Задержка Стоимость в ЕЗ Производительность
6 мс 7,05 ЕЗ

[Q5] — список отметок "Нравится" для записи

Так же, как и при выполнении операции [Q4], мы запрашиваем отметки "Нравится" для нужной записи, а затем получаем для них имена пользователей.

Получение всех отметок &quot;Нравится&quot; к записи и статистическая обработка дополнительных данных

Задержка Стоимость в ЕЗ Производительность
59 мс 58,92 ЕЗ

[Q6] — список "x" самых свежих записей в краткой форме (веб-канал)

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

Получение самых свежих записей и статистическая обработка дополнительных данных

Еще раз отметим, что начальный запрос не использует для контейнера posts фильтр по ключу секции, то есть выполняется как дорогостоящий размноженный запрос. Это даже хуже, так как мы получаем больше результатов и сортируем их с помощью предложения ORDER BY, что влечет за собой дополнительные затраты на единицы запросов.

Задержка Стоимость в ЕЗ Производительность
306 мс 2063,54 ЕЗ

Факторы, влияющие на производительность версии 1

Изучая проблемы с производительностью, которые мы обнаружили в предыдущем разделе, можно выделить два основных класса проблем:

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

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

Версия 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 для соответствующей записи. Так как контейнер posts секционируется по postId, новый элемент (комментарий или отметка "Нравится") располагается в той же логической секции, что и соответствующая запись. Это позволяет нам использовать хранимую процедуру для выполнения нужной операции.

Теперь, когда создается комментарий (операция [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:

Денормализация имен пользователей в контейнере 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?

[Q2] — получение записи

Теперь, когда мы настроили денормализацию, для обработки этого запроса достаточно получить один элемент.

Получение одного элемента из контейнера posts

Задержка Стоимость в ЕЗ Производительность
2 мс 1 ЕЗ

[Q4] — список комментариев к записи

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

Получение всех комментариев для записи

Задержка Стоимость в ЕЗ Производительность
4 мс 7,72 ЕЗ

[Q5] — список отметок "Нравится" для записи

Аналогичный результат достигнут и для перечисления отметок "Нравится".

Получение всех отметок &quot;Нравится&quot; для записи

Задержка Стоимость в ЕЗ Производительность
4 мс 8,92 ЕЗ

Версия 3: обеспечение масштабируемости для всех операций

Изучая достигнутые показатели производительности, мы видим две еще не полностью оптимизированных операции: [Q3] и [Q6]. Эти операции связаны с запросами, которые используют фильтрацию контейнеров по ключу секции.

[Q3] — список записей пользователя в краткой форме

В этот запрос в версии 2 уже были внесены улучшения, позволяющие избежать дополнительных запросов.

Схема, на которой показан запрос для отображения списка записей пользователя в краткой форме.

Но сохранившийся запрос по-прежнему не выполняет фильтрацию контейнера posts по ключу раздела.

К этой ситуации можно подойти достаточно простым способом.

  1. Операция должна применять фильтр по userId, ведь мы хотим получить все записи конкретного пользователя.
  2. Она работает неэффективно, так как применяется к контейнеру posts, который не секционирован по параметру userId.
  3. Совершено очевидно, что эту проблему с производительностью можно решить, выполняя запрос к контейнеру, который уже секционирован по параметру userId.
  4. И он у нас есть: это контейнер 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 в элементе user, чтобы отличать записи от пользователей.
  • Мы также добавили к элементу user поле userId, которое дублирует поле id. Оно нам нужно, так как контейнер users теперь секционируется по userId (а не по id, как ранее).

Чтобы выполнить эту денормализацию, мы снова применяем канал изменений. Теперь мы настроим реагирование по каналу изменений в контейнере posts, чтобы переносить в контейнер users все новые или измененные записи. Кроме того, так как список записей не нужно возвращать с полным содержимым, мы можем усекать их при обработке.

Денормализация с переносом записей в контейнер users

Теперь наш запрос можно направить к контейнеру users и использовать фильтрацию по ключу секции этого контейнера.

Получение всех записей пользователя

Задержка Стоимость в ЕЗ Производительность
4 мс 6,46 ЕЗ

[Q6] — список "x" самых свежих записей в краткой форме (веб-канал)

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

Схема, на которой показан запрос для вывода списка &quot;x&quot; самых последних записей в краткой форме.

Применяя тот же подход, производительность и масштабируемость этого запроса можно увеличить, ограничив область его действия одной секцией. Это вполне достижимо, так как возвращать нужно ограниченный набор элементов. Чтобы заполнить домашнюю страницу нашей платформы блогов достаточно лишь получить 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:

Извлечение самых последних записей

Задержка Стоимость в ЕЗ Производительность
9 мс 16,97 ЕЗ

Заключение

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

V1 V2 V3
[C1] 7 мс / 5,71 ЕЗ 7 мс / 5,71 ЕЗ 7 мс / 5,71 ЕЗ
[Q1] 2 мс / 1 ЕЗ 2 мс / 1 ЕЗ 2 мс / 1 ЕЗ
[C2] 9 мс / 8,76 ЕЗ 9 мс / 8,76 ЕЗ 9 мс / 8,76 ЕЗ
[Q2] 9 мс / 19,54 ЕЗ 2 мс / 1 ЕЗ 2 мс / 1 ЕЗ
[Q3] 130 мс / 619,41 ЕЗ 28 мс / 201,54 ЕЗ 4 мс / 6,46 ЕЗ
[C3] 7 мс / 8,57 ЕЗ 7 мс / 15,27 ЕЗ 7 мс / 15,27 ЕЗ
[Q4] 23 мс / 27,72 ЕЗ 4 мс / 7,72 ЕЗ 4 мс / 7,72 ЕЗ
[C4] 6 мс / 7,05 ЕЗ 7 мс / 14,67 ЕЗ 7 мс / 14,67 ЕЗ
[Q5] 59 мс / 58,92 ЕЗ 4 мс / 8,92 ЕЗ 4 мс / 8,92 ЕЗ
[Q6] 306 мс / 2063,54 ЕЗ 83 мс / 532,33 ЕЗ 9 мс / 16,97 ЕЗ

Мы оптимизировали сценарий с интенсивной нагрузкой на чтение.

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

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

Давайте рассмотрим пример самый существенный из представленных здесь приемов оптимизации. Операция [Q6] теперь требует только 17 ЕЗ вместо 2000 и более. Это достигается путем денормализации записей, повышающей затраты на сохранение каждой записи примерно на 10 ЕЗ. Так как запросы канала обновлений обслуживаются многократно чаще, чем создание или обновление записей, затраты на денормализацию можно считать несущественными по сравнению с увеличением эффективности.

Денормализацию можно применять последовательно

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

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

Дальнейшие действия

Изучив эту вводную статью о моделировании и секционировании данных, вы можете перейти к следующим статьям с дополнительными сведениями о рассмотренных здесь понятиях:

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