Como modelar e particionar dados no Azure Cosmos DB usando um exemplo do mundo real

APLICA-SE A: NoSQL

Este artigo se baseia em vários conceitos do Azure Cosmos DB, tais como modelagem de dados, particionamento e taxa de transferência provisionada para demonstrar como lidar com um exercício de criação de dados do mundo real.

Se normalmente trabalha com bancos de dados relacionais, você provavelmente criou hábitos e intuições sobre como criar um modelo de dados. Por causa das restrições específicas, mas também dos pontos fortes exclusivos do Azure Cosmos DB, a maioria dessas práticas recomendadas não geram bons resultados e podem arrastar você para soluções de qualidade inferior ao ideal. A meta deste artigo é orientá-lo pelo processo completo de modelagem de um caso de uso do mundo real no Azure Cosmos DB, desde a modelagem de itens à colocação de entidades e ao particionamento de contêineres.

Baixe ou exiba um código-fonte gerado pela comunidade que ilustre os conceitos deste artigo.

Importante

Um colaborador da comunidade contribuiu com esse exemplo de código e a equipe do Azure Cosmos DB não dá suporte à manutenção.

O cenário

Para este exercício, vamos considerar o domínio de uma plataforma de blogs em que os usuários podem criar posts. Os usuários também podem curtir esses posts e adicionar comentários a elas.

Dica

Destacamos algumas palavras em itálico; essas palavras identificam o tipo de "coisas" que nosso modelo precisará manipular.

Adicionando mais requisitos para nossa especificação:

  • Uma página frontal exibe um feed de posts criados recentemente,
  • Podemos buscar todos os posts de um usuário, todos os comentários para um post e todas as curtidas para um post,
  • Os posts são retornados com o nome de usuário de seus autores e uma contagem de quantos comentários e curtidas eles têm,
  • Comentários e curtidas também são retornadas com o nome de usuário dos usuários que os criaram,
  • Quando exibidos como listas, os posts só precisam apresentar um resumo truncado de seu conteúdo.

Identificar os principais padrões de acesso

Para começar, podemos dar alguma estrutura para nossa especificação inicial, identificando os padrões de acesso da nossa solução. Ao criar um modelo de dados do Azure Cosmos DB, é importante compreender quais solicitações nosso modelo precisa atender para assegurar que o modelo atenda a essas solicitações com eficiência.

Para tornar o processo geral mais fácil de seguir, categorizamos essas diferentes solicitações como comandos ou consultas, pegando emprestado o vocabulário do CQRS. No CQRS, os comandos são solicitações de gravação (ou seja, intenções para atualizar o sistema) e as consultas são solicitações somente leitura.

Aqui está a lista de solicitações que nossa plataforma expõe:

  • [C1] Criar/editar um usuário
  • [Q1] Recuperar um usuário
  • [C2] Criar/editar um post
  • [Q2] Recuperar um post
  • [Q3] Listar os posts de um usuário na forma abreviada
  • [C3] Criar um comentário
  • [Q4] Listar os comentários de um post
  • [C4] Curtir um post
  • [Q5] Listar as curtidas de um post
  • [Q6] Listar os x posts mais recentes criados na forma abreviada (feed)

Neste estágio, ainda não pensamos sobre os detalhes do que cada entidade (usuário, post etc.) contém. Essa etapa geralmente está entre as primeiras a serem abordadas ao projetar em uma relational store. começamos com a primeira etapa pois precisamos descobrir como essas entidades são traduzidas em termos de tabelas, colunas, chaves estrangeiras, etc. É muito menos uma preocupação com um banco de dados de documentos que não impõe nenhum esquema na gravação.

O principal motivo pelo qual é importante identificar os nossos padrões de acesso desde o início é porque essa lista de solicitações vai ser o nosso conjunto de testes. Sempre que iteramos pelo nosso modelo de dados, percorremos cada uma das solicitações e verificamos os respectivos desempenho e escalabilidade. Calculamos as unidades de solicitação consumidas em cada modelo e as otimizamos. Todos esses modelos usam a política de indexação padrão e você pode substituí-lo 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 contêineres: users e posts.

Contêiner de usuários

Esse contêiner armazena apenas os itens do usuário:

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

Particionamos esse contêiner por id, o que significa que cada partição lógica dentro do contêiner contém apenas um item.

Contêiner de posts

Este contêiner hospeda entidades como posts, comentários e afins:

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

Particionamos esse contêiner por postId, o que significa que cada partição lógica dentro do contêiner contém um post, bem como todos os comentários e serviços para esse post.

Apresentamos uma propriedade type nos itens armazenados nesse contêiner para distinguir entre os três tipos de entidades que esse contêiner hospeda.

Além disso, optamos por fazer referência a dados relacionados em vez de inseri-los (verifique esta seção para obter detalhes sobre esses conceitos) porque:

  • não há nenhum limite superior de quantos posts um usuário pode criar;
  • os posts podem ser arbitrariamente longos;
  • não há limite superior para a quantidade de comentários e curtidas que um post pode ter;
  • queremos adicionar um comentário ou uma curtida a um post sem precisar atualizar o post propriamente dito.

Como é o desempenho do nosso modelo?

Agora é hora de avaliar o desempenho e escalabilidade de nossa primeira versão. Para cada uma das solicitações identificadas anteriormente, medimos o número de unidades de solicitação consumidas e a latência. Essa medida é feita em um conjunto de dados fictício que contém a 100.000 usuários com 5 a 50 posts por usuário e até 25 comentários e 100 curtidas por post.

[C1] Criar/editar um usuário

Essa solicitação é muito fácil de implementar, podemos simplesmente criar ou atualizar um item no contêiner users. As solicitações se espalham bem por todas as partições graças à chave de partição id.

Diagrama de gravação um único item no contêiner de usuários.

Latência Preço da RU Desempenho
7 ms 5.71 RU

[Q1] Recuperar um usuário

A recuperação de um usuário é realizada lendo-se o item correspondente do contêiner users.

Diagrama de recuperação de um único item do contêiner de usuários.

Latência Preço da RU Desempenho
2 ms 1 RU

[C2] Criar/editar um post

Da mesma forma que [C1], temos apenas que gravar o contêiner posts.

Diagrama da gravação de um único item de postagem no contêiner de postagens.

Latência Preço da RU Desempenho
9 ms 8.76 RU

[Q2] Recuperar um post

Começamos recuperando o documento correspondente do contêiner posts. Mas isso não é suficiente. De acordo com nossa especificação, também temos que agregar o nome de usuário do autor da postagem, contagens de comentários e contagens de curtidas para a postagem. As agregações listadas exigem mais três consultas SQL a serem emitidas.

Diagrama de recuperação de um post e agregar dados adicionais.

Cada uma das consultas adicionais filtra a chave de partição de seu respectivo contêiner, que é exatamente o que queremos para maximizar o desempenho e a escalabilidade. Mas eventualmente precisamos executar quatro operações para retornar um único post, portanto, melhoraremos isso em uma próxima iteração.

Latência Preço da RU Desempenho
9 ms 19.54 RU

[Q3] Listar os posts de um usuário na forma abreviada

Primeiro, precisamos recuperar os posts desejados com uma consulta SQL que busca os posts correspondentes a esse usuário específico. Mas também precisamos emitir mais consultas para agregar o nome de usuário do autor e as contagens de comentários e curtidas.

Diagrama de recuperação de todos os posts de um usuário e agregar seus dados adicionais.

Essa implementação apresenta várias desvantagens:

  • as consultas de agregação as contagens de comentários e curtidas precisam ser emitidos para cada post retornado pela primeira consulta,
  • a consulta principal não filtra a chave de partição do contêiner posts, levando a um fan-out e a uma verificação de partição pelo contêiner.
Latência Preço da RU Desempenho
130 ms 619.41 RU

[C3] Criar um comentário

Um comentário é criado escrevendo-se o item correspondente no contêiner posts.

Diagrama da gravação de um único item de comentário no contêiner de postagens.

Latência Preço da RU Desempenho
7 ms 8.57 RU

[Q4] Listar os comentários de um post

Vamos começar com uma consulta que busca todos os comentários para esse post e, mais uma vez, também precisamos agregar nomes de usuário separadamente para cada comentário.

Diagrama de recuperação de todos os comentários de um post e agregar os dados adicionais deles.

Embora a consulta principal filtre na chave de partição do contêiner, agregar os nomes de usuário separadamente causa prejuízo ao desempenho em geral. Aprimoraremos isso mais tarde.

Latência Preço da RU Desempenho
23 ms 27.72 RU

[C4] Curtir um post

Assim como [C3], criamos o item correspondente no contêiner posts.

Diagrama da gravação de um único item (curtir) no contêiner de postagens.

Latência Preço da RU Desempenho
6 ms 7.05 RU

[Q5] Listar as curtidas de um post

Assim como em [Q4], podemos consultar as curtidas desse post e, em seguida, agregar os respectivos nomes de usuário.

Diagrama de recuperação de todas as curtidas de um post e agregar os respectivos dados adicionais.

Latência Preço da RU Desempenho
59 ms 58.92 RU

[Q6] Listar os x posts mais recentes criados na forma abreviada (feed)

Buscamos os posts mais recentes consultando o contêiner posts classificado por data de criação decrescente, depois agregamos os nomes de usuário e contagens de comentários e curtidas para cada um dos posts.

Diagrama de recuperação dos posts mais recentes e agregar seus dados adicionais.

Mais uma vez, nossa consulta inicial não filtra a chave de partição do contêiner posts, o que dispara um fan-out dispendioso. Isso é ainda pior, pois visamos um conjunto de resultados maior e classificamos os resultados com uma cláusula ORDER BY, o que o torna mais caro em termos de unidades de solicitação.

Latência Preço da RU Desempenho
306 ms 2063.54 RU

Refletindo sobre o desempenho do V1

Examinando os problemas de desempenho com os quais nos deparamos na seção anterior, podemos identificar duas classes principais de problemas:

  • algumas solicitações exigem que várias consultas sejam emitidas para coletar todos os dados que precisamos retornar;
  • algumas consultas não filtram a chave de partição dos contêineres aos quais se destinam, levando a um fan-out que impede nossa escalabilidade.

Resolveremos cada um desses problemas, começando com o primeiro deles.

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

Temos que emitir mais solicitações em alguns casos porque os resultados da solicitação inicial não contêm todos os dados que precisamos retornar. A desnormalização de dados resolve esse tipo de problema em nosso conjunto de dados ao trabalhar com um armazenamento de dados não relacional como o Azure Cosmos DB.

Em nosso exemplo, podemos modificar itens de post para adicionar o nome de usuário do autor do post, a contagem de comentários e a contagem de curtidas:

{
    "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 podemos modificar itens de comentário e de curtida para adicionar o nome de usuário correspondente ao usuário 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>"
}

Desnormalização de contagens de comentários e de curtidas

O que queremos alcançar é que, cada vez que adicionarmos um comentário ou uma curtida, possamos também aumentar o commentCount ou o likeCount no post correspondente. Como nosso contêiner postId é particionado por posts, o novo item (comentário ou curtida) e seu post correspondente ficam na mesma partição lógica. Como resultado, podemos usar um procedimento armazenado para executar essa operação.

Durante a criação de um comentário ([C3]), em vez de apenas adicionar um novo item ao contêiner posts, podemos chamar o seguinte procedimento armazenado nesse contêiner:

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

Esse procedimento armazenado usa a ID do post e o corpo do novo comentário como parâmetros, depois:

  • recupera o post
  • incrementa o commentCount
  • substitui o post
  • adiciona o novo comentário

Já 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 em sincronia.

Obviamente, chamamos um procedimento armazenado semelhante ao adicionar novas curtidas para incrementar o likeCount.

Desnormalizar nomes de usuário

Os nomes de usuário exigem uma abordagem diferente, pois os usuários não só ficam em partições diferentes, mas também em um contêiner diferente. Quando precisamos desnormalizar os dados em partições e contêineres, podemos usar o feed de alterações do contêiner de origem.

Em nosso exemplo, usamos o feed de alterações do contêiner users para reagir sempre que os usuários atualizam seus nomes de usuário. Quando isso acontece, podemos propagar a alteração chamando outro procedimento armazenado no contêiner posts:

Diagrama de desnormalização de nomes de usuário no contêiner de 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);
      }
    });
}

Esse procedimento armazenado usa a o novo nome de usuário e a ID do usuário como parâmetros, depois:

  • busca todos os itens correspondentes ao userId (que podem ser posts, comentários ou curtidas)
  • para cada um desses itens
    • substitui o userUsername
    • substitui o item

Importante

Esta operação é cara porque requer que esse procedimento armazenado seja executado em todas as partições do contêiner posts. Vamos supor que a maioria dos usuários escolhe um nome de usuário adequado durante a inscrição e não o altera, então essa 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] Recuperar um post

Agora que nossa desnormalização está em vigor, precisamos apenas buscar um único item para lidar com essa solicitação.

Diagrama de recuperação de um único item do contêiner de postagens desnormalizadas.

Latência Preço da RU Desempenho
2 ms 1 RU

[Q4] Listar os comentários de um post

Aqui, novamente, podemos economizar as solicitações extras que buscaram os nomes de usuário e terminam com uma única consulta que filtra na chave de partição.

Diagrama de recuperação de todos os comentários para uma postagem desnormalizada.

Latência Preço da RU Desempenho
4 ms 7.72 RU

[Q5] Listar as curtidas de um post

Exatamente a mesma situação ao listar as curtidas.

Diagrama de recuperação de todas as curtidas para uma postagem desnormalizada.

Latência Preço da RU Desempenho
4 ms 8.92 RU

V3: Verificar se todas as solicitações são escalonáveis

Ainda há duas solicitações que não otimizamos totalmente ao examinar nossas melhorias gerais de desempenho. Essas solicitações são [Q3] e [Q6]. Elas são as solicitações que envolvem consultas que não filtram na chave de partição dos contêineres aos quais elas se destinam.

[Q3] Listar os posts de um usuário na forma abreviada

Esta solicitação já se beneficia dos aperfeiçoamentos introduzidos na V2, o que poupa consultas adicionais.

Diagrama que mostra a consulta para listar as postagens desnormalizadas de um usuário em forma abreviada.

Mas a consulta restante ainda não está filtrando na chave de partição do contêiner posts.

A maneira de pensar sobre essa situação é simples:

  1. Essa solicitação precisa filtrar no userId porque queremos buscar todos os posts de um usuário específico.
  2. Ela não tem um bom desempenho porque é executada no contêiner posts, que não é particionanda por userId.
  3. Dizendo o óbvio, resolveríamos nosso problema de desempenho executando essa solicitação direcionada a um contêiner particionado com userId.
  4. Acontece que já temos um contêiner assim: o contêiner users!

Portanto, apresentamos um segundo nível de desnormalização ao duplicarmos posts inteiros para o contêiner users. Fazendo isso, efetivamente obtemos uma cópia de nossos posts, apenas particionados em uma dimensão diferente, tornando a recuperação deles mais eficiente por seus userId.

O contêiner users agora contém 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:

  • Apresentamos um campo type no item de usuário para diferenciar usuários de posts,
  • Também adicionamos um campo userId no item de usuário, que é redundante com o campo id, mas é necessário já que o contêiner users agora é particionado com userId (e não por id como anteriormente)

Para alcançar essa desnormalização, podemos usar novamente o feed de alterações. Neste momento, podemos reagir no feed de alterações do contêiner posts para expedir qualquer post novo ou atualizado para o contêiner users. E já que a listagem de posts não exige que o conteúdo completo deles seja retornado, podemos truncá-los no processo.

Diagrama de desnormalização posts no contêiner de usuários.

Agora podemos encaminhar nossa consulta para o contêiner users, filtrando na chave de partição do contêiner.

Diagrama de recuperação de todas as postagens para um usuário desnormalizada.

Latência Preço da RU Desempenho
4 ms 6.46 RU

[Q6] Listar os x posts mais recentes criados na forma abreviada (feed)

Temos de lidar com uma situação semelhante aqui: mesmo depois de poupar as consultas adicionais tornadas desnecessárias pela desnormalização introduzida na V2, a consulta restante não filtra na chave de partição do contêiner:

Diagrama que mostra a consulta para listar as postagens x mais recentes criadas em forma abreviada.

Seguindo a mesma abordagem, maximizar o desempenho e a escalabilidade desta solicitação requer que ela atinja somente uma partição. Isso é concebível porque precisamos apenas retornar um número limitado de itens. Para preencher a home page da nossa plataforma de blog, é necessário apenas obter os 100 posts mais recentes, sem a necessidade de paginar através de todo o conjunto de dados.

Portanto, para otimizar essa última solicitação, introduzimos um terceiro contêiner em nosso design, totalmente dedicado ao atendimento dessa solicitação. Podemos desnormalizar nossos posts para esse novo contêiner 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>"
}

O campo type particiona esse contêiner, que está sempre post em nossos itens. Fazer isso garante que todos os itens nesse contêiner fiquem na mesma partição.

Para alcançar a desnormalização, precisamos apenas nos conectar ao pipeline do feed de alterações que introduzimos anteriormente para expedir os posts para esse novo contêiner. Uma coisa importante para se ter em mente é que precisamos verificar se armazenamos apenas os 100 posts mais recentes, caso contrário, o conteúdo do contêiner pode crescer além do tamanho máximo de uma partição. Essa limitação pode ser implementada chamando um pós-gatilho toda vez que um documento é adicionado no contêiner:

Diagrama de desnormalização de posts no contêiner do feed.

Aqui está o corpo do pós-gatilho 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);
        });
    }
  }
}

A etapa final é redirecionar a nossa consulta ao nosso novo contêiner feed:

Diagrama de recuperação das postagens mais recentes.

Latência Preço da RU Desempenho
9 ms 16.97 RU

Conclusão

Vamos dar uma olhada em aprimoramentos de desempenho e escalabilidade gerais que introduzimos nas diferentes versões do nosso projeto.

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

Nós otimizamos um cenário de leitura intensa

Você talvez tenha notado que concentramos nossos esforços para melhorar o desempenho das solicitações de leitura (consultas), em detrimento das solicitações de gravação (comandos). Em muitos casos, as operações de gravação agora disparam uma desnormalização subsequente por meio de feeds de alterações, o que as torna mais onerosas computacionalmente e faz com que levem mais tempo para se materializar.

Justificamos esse foco no desempenho de leitura porque uma plataforma de blog (como a maioria dos aplicativos de redes sociais) é pesada para leitura. Uma carga de trabalho de leitura pesada indica que a quantidade de solicitações de leitura que ela precisa atender geralmente é muito maior do que o número de solicitações de gravação. Portanto, faz sentido fazer solicitações de gravação mais dispendiosas para permitir que solicitações de leitura sejam mais baratas e tenham melhor desempenho.

Se verificarmos a otimização mais extrema que fizemos, [Q6] passou de 2000+ RUs para apenas 17 RUs; alcançamos isso desnormalizando posts a um custo de cerca de 10 RUs por item. Já que poderíamos atender a muito mais solicitações de feed do que a criação ou atualizações de posts, o custo dessa desnormalização é insignificante ao considerar a economia geral.

A desnormalização pode ser aplicada de forma incremental

Os aprimoramentos de escalabilidade explorados neste artigo envolvem a desnormalização e duplicação de dados em todo o conjunto de dados. Deve-se observar que essas otimizações não precisam ser colocadas em vigor no primeiro dia. Consultas que filtram nas chaves de partição têm um desempenho melhor em grande escala, mas consultas entre partições podem ser aceitáveis se são chamadas raramente ou direcionadas a um conjunto de dados limitado. Se você está apenas construindo um protótipo ou lançando um produto com uma base de usuários pequena e controlada, provavelmente pode deixar essas melhorias para mais tarde. O mais importante é monitorar a performance do seu modelo para decidir se e quando é hora de trazê-los.

O feed de alterações que usamos para distribuir atualizações para outros contêineres armazena todas essas atualizações de maneira persistente. A persistência torna possível solicitar todas as atualizações desde a criação do contêiner e inicializar exibições desnormalizadas como uma operação de atualização única, mesmo se o sistema já tem muitos dados.

Próximas etapas

Após esta introdução ao particionamento e à modelagem de dados de caráter prático, você talvez queira verificar os artigos a seguir para analisar os conceitos abordados: