Como modelar e criar partições de dados no Azure Cosmos DB com um exemplo do mundo real

APLICA-SE A: NoSQL

Este artigo baseia-se em vários conceitos do Azure Cosmos DB, como modelação de dados, criação de partições e débito aprovisionado para demonstrar como lidar com um exercício de design de dados no mundo real.

Se normalmente trabalha com bases de dados relacionais, provavelmente criou hábitos e intuições sobre como conceber um modelo de dados. Devido às restrições específicas, mas também aos pontos fortes exclusivos do Azure Cosmos DB, a maioria destas melhores práticas não se traduz bem e pode arrastá-lo para soluções subótimas. O objetivo deste artigo é orientá-lo ao longo do processo completo de modelação de um caso de utilização real no Azure Cosmos DB, desde a modelação de itens até à colocação de entidades e à criação de partições de contentores.

Transfira ou veja um código fonte gerado pela comunidade que ilustra os conceitos deste artigo.

Importante

Um contribuidor da comunidade contribuiu com este exemplo de código e a equipa do Azure Cosmos DB não suporta a manutenção.

Cenário

Para este exercício, vamos considerar o domínio de uma plataforma de blogues onde os utilizadores podem criar publicações. Os utilizadores também podem gostar e adicionar comentários a essas publicações.

Dica

Realçámos algumas palavras em itálico; estas palavras identificam o tipo de "coisas" que o nosso modelo terá de manipular.

Adicionar mais requisitos à nossa especificação:

  • Uma primeira página apresenta um feed de publicações criadas recentemente,
  • Podemos obter todas as publicações de um utilizador, todos os comentários de uma publicação e todos os gostos de uma publicação,
  • As publicações são devolvidas com o nome de utilizador dos autores e uma contagem de quantos comentários e gostos têm,
  • Os comentários e gostos também são devolvidos com o nome de utilizador dos utilizadores que os criaram,
  • Quando apresentadas como listas, as publicações só têm de apresentar um resumo truncado dos respetivos conteúdos.

Identificar os principais padrões de acesso

Para começar, damos alguma estrutura à nossa especificação inicial ao identificar os padrões de acesso da nossa solução. Ao conceber um modelo de dados para o Azure Cosmos DB, é importante compreender que pedidos o nosso modelo tem de servir para garantir que o modelo serve esses pedidos de forma eficiente.

Para tornar o processo geral mais fácil de seguir, categorizamos esses diferentes pedidos como comandos ou consultas, pedindo algum vocabulário emprestado ao CQRS. No CQRS, os comandos são pedidos de escrita (ou seja, intenções para atualizar o sistema) e as consultas são pedidos só de leitura.

Eis a lista de pedidos que a nossa plataforma expõe:

  • [C1] Criar/editar um utilizador
  • [Q1] Obter um utilizador
  • [C2] Criar/editar uma publicação
  • [Q2] Obter uma mensagem
  • [Q3] Listar as publicações de um utilizador em formato abreviado
  • [C3] Criar um comentário
  • [Q4] Listar os comentários de uma mensagem
  • [C4] Como uma mensagem
  • [Q5] Listar os gostos de uma mensagem
  • [Q6] Listar as mensagens x mais recentes criadas em formato curto (feed)

Nesta fase, não pensamos nos detalhes do que cada entidade (utilizador, post, etc.) contém. Normalmente, este passo está entre os primeiros a ser abordados ao conceber um arquivo relacional. Começamos por este passo primeiro porque temos de descobrir como essas entidades se traduzem em termos de tabelas, colunas, chaves externas, etc. É muito menos preocupante com uma base de dados de documentos que não impõe qualquer esquema na escrita.

A principal razão pela qual é importante identificar os nossos padrões de acesso desde o início é porque esta lista de pedidos será o nosso conjunto de testes. Sempre que iteramos sobre o nosso modelo de dados, passamos por cada um dos pedidos e verificamos o seu desempenho e escalabilidade. Calculamos as unidades de pedido consumidas em cada modelo e otimizamo-las. Todos estes modelos utilizam a política de indexação predefinida e pode substitui-la ao indexar propriedades específicas, o que pode melhorar ainda mais o consumo e a latência de RU.

V1: uma primeira versão

Começamos com dois contentores: users e posts.

Contentor de utilizadores

Este contentor só armazena itens de utilizador:

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

Particionamos este contentor por id, o que significa que cada partição lógica nesse contentor contém apenas um item.

Contentor de mensagens

Este contentor aloja entidades como publicações, comentários e gostos:

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

Partimos este contentor por postId, o que significa que cada partição lógica nesse contentor contém uma mensagem, todos os comentários dessa publicação e todos os gostos dessa publicação.

Introduzimos uma type propriedade nos itens armazenados neste contentor para distinguir entre os três tipos de entidades que este contentor aloja.

Além disso, escolhemos referenciar dados relacionados em vez de os incorporar (consulte esta secção para obter detalhes sobre estes conceitos) porque:

  • não existe um limite superior para quantas publicações um utilizador pode criar,
  • as publicações podem ser arbitrariamente longas,
  • não existe um limite superior para quantos comentários e gostos uma publicação pode ter,
  • queremos poder adicionar um comentário ou um gosto a uma publicação sem ter de atualizar a publicação em si.

Qual é o desempenho do nosso modelo?

Chegou a altura de avaliar o desempenho e a escalabilidade da nossa primeira versão. Para cada um dos pedidos previamente identificados, medimos a latência e quantas unidades de pedido consome. Esta medição é feita num conjunto de dados fictício que contém 100 000 utilizadores com 5 a 50 publicações por utilizador e até 25 comentários e 100 gostos por publicação.

[C1] Criar/editar um utilizador

Este pedido é simples de implementar à medida que criamos ou atualizamos um item no users contentor. Os pedidos estão bem distribuídos por todas as partições graças à id chave de partição.

Diagrama de escrita de um único item no contentor dos utilizadores.

Latência Custo de RU Desempenho
7 ms 5.71 RU

[Q1] Obter um utilizador

A obtenção de um utilizador é feita ao ler o item correspondente a users partir do contentor.

Diagrama de obtenção de um único item a partir do contentor dos utilizadores.

Latência Custo de RU Desempenho
2 ms 1 RU

[C2] Criar/editar uma publicação

Da mesma forma que [C1], só temos de escrever no posts contentor.

Diagrama de escrita de um único item de publicação no contentor de mensagens.

Latência Custo de RU Desempenho
9 ms 8.76 RU

[Q2] Obter uma mensagem

Começamos por obter o documento correspondente do posts contentor. Mas isso não é suficiente, de acordo com a nossa especificação, também temos de agregar o nome de utilizador do autor da publicação, contagens de comentários e contagens de gostos para a publicação. As agregações listadas requerem que sejam emitidas mais 3 consultas SQL.

Diagrama de obtenção de uma publicação e agregação de dados adicionais.

Cada uma das mais consultas filtra a chave de partição do respetivo contentor, que é exatamente o que queremos maximizar o desempenho e a escalabilidade. Mas, eventualmente, temos de realizar quatro operações para devolver uma única mensagem, pelo que iremos melhorá-lo numa próxima iteração.

Latência Custo de RU Desempenho
9 ms 19.54 RU

[T3] Listar as publicações de um utilizador em formato abreviado

Primeiro, temos de obter as mensagens pretendidas com uma consulta SQL que obtém as publicações correspondentes a esse utilizador específico. Mas também temos de emitir mais consultas para agregar o nome de utilizador do autor e as contagens de comentários e gostos.

Diagrama da obtenção de todas as mensagens para um utilizador e da agregação dos dados adicionais.

Esta implementação apresenta muitas desvantagens:

  • as consultas que agregam as contagens de comentários e gostos têm de ser emitidas para cada publicação devolvida pela primeira consulta,
  • a consulta principal não filtra a chave de partição do posts contentor, o que leva a uma eliminação de ventoinhas e a uma análise de partições no contentor.
Latência Custo de RUs Desempenho
130 ms 619.41 RU

[C3] Criar um comentário

É criado um comentário ao escrever o item correspondente no posts contentor.

Diagrama de escrita de um único item de comentário no contentor de mensagens.

Latência Custo de RUs Desempenho
7 ms 8.57 RU

[T4] Listar os comentários de uma mensagem

Começamos com uma consulta que obtém todos os comentários dessa mensagem e, mais uma vez, também precisamos de agregar nomes de utilizador separadamente para cada comentário.

Diagrama de obtenção de todos os comentários de uma publicação e agregação dos respetivos dados adicionais.

Embora a consulta principal filtre a chave de partição do contentor, a agregação dos nomes de utilizador penaliza separadamente o desempenho geral. Melhoramos isso mais tarde.

Latência Custo de RUs Desempenho
23 ms 27.72 RU

[C4] Gostar de uma mensagem

Tal como [C3], criamos o item correspondente no posts contentor.

Diagrama de escrita de um único item (como) no contentor de mensagens.

Latência Custo de RUs Desempenho
6 ms 7.05 RU

[Q5] Listar os gostos de uma publicação

Tal como [Q4], consultamos os gostos dessa publicação e, em seguida, agregamos os nomes de utilizador.

Diagrama de obtenção de todos os gostos para uma publicação e agregação dos respetivos dados adicionais.

Latência Custo de RUs Desempenho
59 ms 58.92 RU

[T6] Listar as x publicações mais recentes criadas em formato curto (feed)

Obtemos as publicações mais recentes ao consultar o posts contentor ordenado por data de criação descendente e, em seguida, agregar nomes de utilizador e contagens de comentários e gostos para cada uma das publicações.

Diagrama de obtenção de publicações mais recentes e agregação dos respetivos dados adicionais.

Mais uma vez, a nossa consulta inicial não filtra a chave de partição do contentor, o posts que aciona uma saída dispendiosa. Esta é ainda pior, uma vez que visamos um conjunto de resultados maior e ordenamos os resultados com uma ORDER BY cláusula, o que o torna mais caro em termos de unidades de pedido.

Latência Custo de RUs Desempenho
306 ms 2063.54 RU

Refletir sobre o desempenho da V1

Analisando os problemas de desempenho que enfrentamos na secção anterior, podemos identificar duas classes principais de problemas:

  • alguns pedidos requerem que sejam emitidas várias consultas para recolher todos os dados que precisamos de devolver,
  • algumas consultas não filtram a chave de partição dos contentores visados, o que leva a um fan-out que impede a nossa escalabilidade.

Vamos resolver cada um desses problemas, começando pelo primeiro.

V2: Introdução à desnormalização para otimizar as consultas de leitura

O motivo pelo qual temos de emitir mais pedidos em alguns casos é porque os resultados do pedido inicial não contêm todos os dados que precisamos de devolver. A desnormalização de dados resolve este tipo de problema no nosso conjunto de dados ao trabalhar com um arquivo de dados não relacional, como o Azure Cosmos DB.

No nosso exemplo, modificamos os itens de publicação para adicionar o nome de utilizador do autor da mensagem, a contagem de comentários e a contagem de gostos:

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

Também modificamos os itens de comentário e gosto para adicionar o nome de utilizador do utilizador que os criou:

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

Desnormalizar contagens de comentários e gostos

O que queremos alcançar é que sempre que adicionamos um comentário ou um like, também incrementamos o commentCount ou o likeCount na mensagem correspondente. À medida postId que particiona o nosso posts contentor, o novo item (comentário ou gosto) e a respetiva mensagem correspondente encontram-se na mesma partição lógica. Como resultado, podemos utilizar um procedimento armazenado para efetuar essa operação.

Quando cria um comentário ([C3]), em vez de adicionar apenas um novo item no posts contentor, chamamos o seguinte procedimento armazenado nesse contentor:

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
          );
        }
      );
    })
}

Este procedimento armazenado utiliza o ID da mensagem e o corpo do novo comentário como parâmetros e, em seguida:

  • obtém a mensagem
  • incrementa o commentCount
  • substitui a mensagem
  • adiciona o novo comentário

À medida que os procedimentos armazenados são executados como transações atómicas, o valor de commentCount e o número real de comentários permanecem sempre sincronizados.

Obviamente, chamamos um procedimento armazenado semelhante ao adicionar novos gostos para incrementar o likeCount.

Desnormalizar nomes de utilizador

Os nomes de utilizador requerem uma abordagem diferente, uma vez que os utilizadores não só se sentam em partições diferentes, como também num contentor diferente. Quando tivermos de desnormalizar dados entre partições e contentores, podemos utilizar o feed de alterações do contentor de origem.

No nosso exemplo, utilizamos o feed de alterações do users contentor para reagir sempre que os utilizadores atualizam os nomes de utilizador. Quando isso acontece, propagamos a alteração ao chamar outro procedimento armazenado no posts contentor:

Diagrama de desnormalização de nomes de utilizador no contentor de mensagens.

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);
      }
    });
}

Este procedimento armazenado utiliza o ID do utilizador e o novo nome de utilizador do utilizador como parâmetros e, em seguida:

  • obtém todos os itens correspondentes a userId (que podem ser publicações, comentários ou gostos)
  • para cada um desses itens
    • substitui o userUsername
    • substitui o item

Importante

Esta operação é dispendiosa porque requer que este procedimento armazenado seja executado em todas as partições do posts contentor. Partimos do princípio de que a maioria dos utilizadores escolhe um nome de utilizador adequado durante a inscrição e nunca o altera, pelo que esta atualização será executada muito raramente.

Quais são os ganhos de desempenho do V2?

Vamos falar sobre alguns dos ganhos de desempenho da V2.

[Q2] Obter uma mensagem

Agora que a nossa desnormalização está implementada, só temos de obter um único item para processar esse pedido.

Diagrama da obtenção de um único item do contentor de mensagens desnormalizadas.

Latência Custo de RUs Desempenho
2 ms 1 RU

[T4] Listar os comentários de uma mensagem

Aqui, mais uma vez, podemos poupar os pedidos adicionais que foram obtidos pelos nomes de utilizador e acabar com uma única consulta que filtra a chave de partição.

Diagrama da obtenção de todos os comentários de uma mensagem desnormalizada.

Latência Custo de RUs Desempenho
4 ms 7.72 RU

[Q5] Listar os gostos de uma publicação

Exatamente a mesma situação ao listar os gostos.

Diagrama de obtenção de todos os gostos para uma mensagem desnormalizada.

Latência Custo de RUs Desempenho
4 ms 8.92 RU

V3: garantir que todos os pedidos são dimensionáveis

Ainda existem dois pedidos que ainda não otimizamos totalmente quando analisamos as nossas melhorias gerais de desempenho. Estes pedidos são [T3] e [T6]. São os pedidos que envolvem consultas que não filtram a chave de partição dos contentores visados.

[T3] Listar as publicações de um utilizador em formato abreviado

Este pedido já beneficia das melhorias introduzidas no V2, o que poupa mais consultas.

Diagrama que mostra a consulta para listar as mensagens desnormalizadas de um utilizador em formato curto.

Contudo, a consulta restante ainda não está a filtrar a chave de partição do posts contentor.

A forma de pensar nesta situação é simples:

  1. Este pedido tem de ser filtrado por userId querermos obter todas as publicações de um determinado utilizador.
  2. Não tem um bom desempenho porque é executado no posts contentor, que não tem userId a criação de partições.
  3. Afirmando o óbvio, resolveríamos o nosso problema de desempenho ao executar este pedido num contentor particionado com userId.
  4. Acontece que já temos um contentor deste tipo: o users contentor!

Por isso, introduzimos um segundo nível de desnormalização ao duplicar mensagens inteiras no users contentor. Ao fazê-lo, obtemos efetivamente uma cópia das nossas publicações, apenas particionadas numa dimensão diferente, tornando-as muito mais eficientes para obter através do respetivo userId.

O users contentor contém agora dois tipos de itens:

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

Neste exemplo:

  • Introduzimos um type campo no item de utilizador para distinguir os utilizadores de mensagens,
  • Também adicionámos um userId campo no item de utilizador, que é redundante com o id campo, mas é necessário, uma vez que o users contentor está agora particionado com userId (e não id como anteriormente)

Para alcançar essa desnormalização, utilizamos novamente o feed de alterações. Desta vez, reagimos ao feed de alterações do posts contentor para enviar qualquer mensagem nova ou atualizada para o users contentor. E como a listagem de mensagens não requer a devolução do conteúdo completo, podemos truncá-las no processo.

Diagrama de desnormalização de mensagens no contentor dos utilizadores.

Agora, podemos encaminhar a nossa consulta para o users contentor, filtrando a chave de partição do contentor.

Diagrama de obtenção de todas as mensagens para um utilizador desnormalizado.

Latência Custo de RUs Desempenho
4 ms 6.46 RU

[T6] Listar as x publicações mais recentes criadas em formato curto (feed)

Temos de lidar com uma situação semelhante aqui: mesmo depois de poupar mais consultas deixadas desnecessárias pela desnormalização introduzida na V2, a consulta restante não filtra a chave de partição do contentor:

Diagrama que mostra a consulta para listar as x publicações mais recentes criadas num formato curto.

Seguindo a mesma abordagem, maximizar o desempenho e a escalabilidade deste pedido requer que atinja apenas uma partição. Só é possível atingir uma única partição porque só temos de devolver um número limitado de itens. Para preencher a home page da nossa plataforma de blogues, só precisamos de obter as 100 mensagens mais recentes, sem a necessidade de paginar através de todo o conjunto de dados.

Assim, para otimizar este último pedido, introduzimos um terceiro contentor na nossa conceção, inteiramente dedicado a servir este pedido. Desnormalizamos as nossas publicações para esse novo feed contentor:

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

O type campo particiona este contentor, que está sempre post nos nossos itens. Ao fazê-lo, garante que todos os itens neste contentor ficarão na mesma partição.

Para alcançar a desnormalização, só temos de ligar ao pipeline do feed de alterações que introduzimos anteriormente para enviar as mensagens para esse novo contentor. Uma coisa importante a ter em conta é que temos de nos certificar de que armazenamos apenas as 100 mensagens mais recentes; caso contrário, o conteúdo do contentor pode aumentar para além do tamanho máximo de uma partição. Esta limitação pode ser implementada ao chamar um pós-acionador sempre que um documento é adicionado ao contentor:

Diagrama de desnormalização de mensagens no contentor de feed.

Eis o corpo do pós-acionador que trunca a coleção:

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);
        });
    }
  }
}

O passo final é reencaminhar a nossa consulta para o nosso novo feed contentor:

Diagrama da obtenção das publicações mais recentes.

Latência Custo de RUs Desempenho
9 ms 16.97 RU

Conclusão

Vejamos os melhoramentos globais de desempenho e escalabilidade que introduzimos sobre as diferentes versões do nosso design.

V1 V2 V3
[C1] 7 ms/ 5.71 RU 7 ms/ 5.71 RU 7 ms/ 5.71 RU
[T1] 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
[T3] 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
[T4] 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
[T6] 306 ms/ 2063.54 RU 83 ms/ 532.33 RU 9 ms/ 16.97 RU

Otimizámos um cenário de leitura intensiva

Pode ter reparado que concentramos os nossos esforços para melhorar o desempenho dos pedidos de leitura (consultas) em detrimento de pedidos de escrita (comandos). Em muitos casos, as operações de escrita acionam agora a desnormalização subsequente através de feeds de alterações, o que as torna mais dispendiosas computacionalmente e mais longas para se materializarem.

Justificamos este foco no desempenho de leitura pelo facto de uma plataforma de blogues (como a maioria das aplicações sociais) ser de leitura intensiva. Uma carga de trabalho de leitura intensiva indica que a quantidade de pedidos de leitura que tem de servir é geralmente ordens de magnitude superiores ao número de pedidos de escrita. Por isso, faz sentido tornar os pedidos de escrita mais dispendiosos para permitir que os pedidos de leitura sejam mais baratos e com melhor desempenho.

Se observarmos a otimização mais extrema que fizemos, [Q6] passou de mais de 2000 RUs para apenas 17 RUs; conseguimos isso ao desnormalizar publicações com um custo de cerca de 10 RUs por item. Como serviríamos muito mais pedidos de feed do que a criação ou atualização de publicações, o custo desta desnormalização é insignificante tendo em conta as poupanças globais.

A desnormalização pode ser aplicada incrementalmente

As melhorias de escalabilidade que explorámos neste artigo envolvem a desnormalização e a duplicação de dados no conjunto de dados. Tenha em atenção que estas otimizações não têm de ser implementadas no dia 1. As consultas que filtram chaves de partição têm um melhor desempenho em escala, mas as consultas entre partições podem ser aceitáveis se forem chamadas raramente ou num conjunto de dados limitado. Se estiver apenas a criar um protótipo ou a iniciar um produto com uma base de utilizador pequena e controlada, provavelmente poderá poupar essas melhorias para mais tarde. O que é importante, então, é monitorizar o desempenho do seu modelo para que possa decidir se e quando é altura de os trazer.

O feed de alterações que utilizamos para distribuir atualizações para outros contentores armazena todas essas atualizações de forma persistente. Esta persistência permite pedir todas as atualizações desde a criação das vistas desnormalizadas do contentor e do bootstrap como uma operação de recuperação única, mesmo que o seu sistema já tenha muitos dados.

Passos seguintes

Após esta introdução à modelação e criação de partições de dados práticas, poderá querer consultar os seguintes artigos para rever os conceitos que abordámos: