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

ОБЛАСТЬ ПРИМЕНЕНИЯ: 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 самых свежих записей в краткой форме (веб-канал).

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

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

Версия 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 .

Схема записи одного элемента в контейнер пользователей.

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

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

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

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

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

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

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

Схема записи одного элемента записи в контейнер записей.

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

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

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

Схема получения записи и агрегирования дополнительных данных.

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

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

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

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

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

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

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

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

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

Схема записи одного элемента комментария в контейнер записей.

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

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

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

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

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

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

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

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

Схема записи одного (например) элемента в контейнер записей.

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

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

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

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

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

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

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

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

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

Задержка Стоимость в ЕЗ Производительность
306 Ms 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 для соответствующей записи. Как 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] — получение записи

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

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

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

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

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

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

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

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

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

Схема получения всех лайк для денормализованного поста.

Задержка Стоимость в ЕЗ Производительность
4 Ms 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 поле в элементе пользователя, чтобы отличать пользователей от записей.
  • Мы также добавили userId поле в элемент пользователя, которое является избыточным вместе с полем id , но является обязательным, так как users контейнер теперь секционирован с userId (а не id как ранее).

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

Схема денормализации записей в контейнер пользователей.

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

Схема получения всех записей для денормализованного пользователя.

Задержка Стоимость в ЕЗ Производительность
4 Ms 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:

Схема получения последних записей.

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

Заключение

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

V1 V2 V3
[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

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

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

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

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

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

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

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

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

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