Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Saiba por que o MongoDB foi selecionado como um líder no 2024 Gartner_Magic Quadrupnt()
Desenvolvedor do MongoDB
Centro de desenvolvedores do MongoDB
chevron-right
Produtos
chevron-right
MongoDB
chevron-right

O custo de não conhecer o MongoDB (V5RX) - Parte 2

Artur Costa26 min read • Published Jan 29, 2025 • Updated Jan 29, 2025
Node.jsFramework de agregaçãoJavaScriptMongoDB
APLICATIVO COMPLETO
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Artigo
star-empty
star-empty
star-empty
star-empty
star-empty
Esta é a segunda parte da série "The Cost of Not Knowing MongoDB, ", na qual examinamos várias maneiras de modelar nossos esquemas do MongoDB para o mesmo aplicação e ter desempenhos diferentes. Na primeira parte da série, concatenamos campos, alteramos tipos de dados e reduzimos os nomes de campo para melhorar o desempenho do aplicação . Nesta segunda parte, conforme discutido nos problemas e melhorias do appV4, os ganhos de desempenho serão alcançados analisando o comportamento do aplicação e como ele armazena e lê seus dados, levando-nos ao uso do Padrão de Bucket e o Padrão computado.

Versão do aplicativo 5 Revisão 0 e Revisão 1 (appV5R0 e appV5R1): uma maneira simples de usar o Padrão de Bucket

Ao gerar o relatório de oneYear totais do, a Get Reports função precisará recuperar uma média de 60 documentos e, no pior cenário, 365 documentos. Para acessar cada documento, uma entrada de índice deve ser visitada e uma operação de leitura de disco deve ser executada.
Uma maneira de reduzir o número de entradas de índice e documentos recuperados para gerar o relatório é usar o Padrão de Bucket. De acordo com a documentação do MongoDB , "O padrão de bucket separa longas séries de dados em objetos distintos. A separação de grandes séries de dados em grupos menores pode melhorar os padrões de acesso a queries e simplificar a lógica do aplicação ."
Observando nosso aplicação da perspectiva do Bucket Pattern, até agora, agrupamos nossos dados diariamente por um usuário, cada documento contendo os totais de status de um usuário em um dia. Para as duas versões do aplicação apresentados nesta seção, appV5R0 e , agruparemos os appV5R1 dados por mês (appV5R0) e por trimestre (appV5R1).
Como essas são nossas primeiras implementações usando o Padrão de Bucket, vamos torná-lo o mais simples possível.
appV5R0Para, cada documento agrupa os eventos por mês e usuário. Cada documento terá um campo de tipo array chamado items para o qual cada event documento será enviado. O documento do evento enviado para a array terá os nomes dos campo de status abreviados para sua primeira letra, da mesma forma que fizemos em,appV3 appV4, e date ao qual o evento se refere.
O _id campo terá uma lógica semelhante à utilizada em appV4, com os valores de key e date concatenados e armazenados como informações hexadecimais/binárias. A diferença é o date valor de — em vez de ser composto por ano, mês e dia (YYYYMMDD) — terá apenas ano e mêsYYYYMM(), pois estamos agrupando os dados por mês.
Para appV5R1, temos quase a mesma implementação que appV5R0, com a diferença de que agruparemos os eventos por trimestre, e o date valor usado para gerar o _id campo será composto por ano e trimestre (YYYYQQ) em vez de ano e mês (YYYYMM).
Para construir o _id campo com base nos key valores e para dateappV5R0, a seguinte função Typescript foi criada:
1const buildId = (key: string, date: Date): Buffer => {
2 const [YYYY, MM] = date.toISOString().split();
3
4 return Buffer.from(`${key}${YYYY}${MM}`, 'hex');
5};
Para construir o _id campo com base nos key valores e para dateappV5R1, as seguintes funções do Typescript foram criadas:
1const getQQ = (date: Date): string => {
2 const month = Number(getMM(date));
3
4 if (month >= 1 && month <= 3) return '01';
5 else if (month >= 4 && month <= 6) return '02';
6 else if (month >= 7 && month <= 9) return '03';
7 else return '04';
8};
9
10const buildId = (key: string, date: Date): Buffer => {
11 const [YYYY] = date.toISOString().split('-');
12 const QQ = getQQ(date);
13
14 return Buffer.from(`${key}${YYYY}${QQ}`, 'hex');
15};

Esquema

As implementações de aplicação apresentados acima teriam o seguinte esquema de documento Typescript SchemaV5R0 denominado:
1type SchemaV5R0 = {
2 _id: Buffer;
3 items: Array<{
4 date: Date;
5 a?: number;
6 n?: number;
7 p?: number;
8 r?: number;
9 }>;
10};

Upsert em massa

Com base na especificação apresentada, temos a seguinte operação em massa updateOne para cada event gerado pelo aplicação:
1const opeartion = {
2 updateOne: {
3 filter: { _id: buildId(event.key, event.date) },
4 update: {
5 $push: {
6 items: {
7 date: event.date,
8 a: event.approved,
9 n: event.noFunds,
10 p: event.pending,
11 r: event.rejected,
12 },
13 },
14 },
15 upsert: true,
16 },
17};
A updateOne operação acima filtrará/pesquisa o documento com o _id campo composto pela concatenação do evento key e do evento month/quarter e para $push o campo da items array um documento com o evento date e status.

Obter relatórios

serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para concluir a operaçãoGet Reports. Cada intervalo de data terá o seguinte pipeline, com apenas a data utilizada na função buildId sendo diferente:
1const pipeline = [
2 {
3 $match: {
4 _id: {
5 $gte: buildId(request.key, Date.now() - oneYear),
6 $lte: buildId(request.key, Date.now()),
7 },
8 },
9 },
10 {
11 $unwind: {
12 path: '$items',
13 },
14 },
15 {
16 $match: {
17 'items.date': { $gte: Date.now() - oneYear, $lt: Date.now() },
18 },
19 },
20 {
21 $group: {
22 _id: null,
23 approved: { $sum: '$items.a' },
24 noFunds: { $sum: '$items.n' },
25 pending: { $sum: '$items.p' },
26 rejected: { $sum: '$items.r' },
27 },
28 },
29 { $project: { _id: 0 } },
30];
O primeiro estágio, $match, filtrará a _id faixa pelo campo . O limite inferior do nosso intervalo, $gte, será gerado usando a buildId função com o key do usuário para o qual queremos o relatório e o date um ano antes do dia em que o relatório foi solicitado. O limite superior do nosso intervalo, $lte, será gerado de forma semelhante ao limite inferior, mas o date fornecido será o dia em que o relatório foi solicitado. Um ponto de atenção nesse estágio é que o resultado de buildId contém informações por mês/trimestre, não por dia, pois precisamos criar o relatório, portanto, será necessária uma filtragem adicional por dia.
O segundo estágio, $unwind, desconstruirá o campo de array a items partir dos documentos de entrada para gerar um documento para cada elemento da array.
O terceiro estágio, $match, filtrará todos os documentos entre o intervalo de datas do relatório. Pode-se ver que já filtramos por data, mas, conforme apresentado na explicação do primeiro estágio, filtramos por mês/trimestre e, para gerar o relatório, precisamos filtrar por dia.
O quarto estágio, $group, agrupará todos os documentos em apenas um (_id: null) e, durante esse processo, $sum os valores de status, gerando os totais de status para o relatório.
O quinto estágio, $project, removerá apenas o _id campo do documento resultante.

Índices

Como essa implementação usará o _id campo para suas operações, ela não precisará de um índice extra para dar suporte às operações Bulk Upsert Get Reports e.

Scenario

Inserindo os 500 milhões de documentos de evento para o cenário inicial nas collections appV5R0 e appV5R1 com o esquema e a Bulk Upsert função apresentados acima, além de apresentar os valores das versões anteriores, temos a seguinte collection stats:
coleçãoDocumentostamanho de dadosTamanho do documentoTamanho do armazenamentoÍndicesTamanho do Índice
appV1359.639.62239,58GB119B8.78GB220.06GB
appV2359.614.53641.92GB126B10.46GB216.66GB
appV3359.633.37628.7GB86B8.96GB216.37GB
appV4359.615.27919.66GB59B6.69GB19.5GB
appV5R095.350.43119.19GB217B5.06GB12.95GB
appV5R133.429.64915.75GB506B4.04GB11.09GB
Calculando o event stats para appV5R0 e e appV5R1 também apresentam os valores das versões anteriores, temos o seguinte:
coleçãoTamanho dos Dados/ventosTamanho/ventos do ÍndiceTamanho total/ventos
appV185B43.1B128.1B
appV290B35.8B125.8B
appV361.6B35.2B96.8B
appV442.2B20.4B62.6B
appV5R041.2B6.3B47.5B
appV5R133.8B2.3B36.1B
Analisando as tabelas acima, podemos ver que passando appV4 de para ,appV5R0 praticamente não tivemos melhorias ao analisar Data Size, mas ao considerar Index Size, a melhoria foi bastante significativa. O tamanho do índice para appV5R0 é 69% do tamanho de appV4.
Ao considerar ir de appV4 para appV5R1, os ganhos são ainda mais expressivos. Nesse caso, reduzimos o Data Size em 20% e o Index Size em 89%.
Analisando o event stats, tivemos melhorias consideráveis no Total Size/events, mas o que realmente chama a atenção é a melhoria no Index Size/events, que é três vezes menor para appV5R0 e nove vezes menor para appV5R1.
Essa grande redução no tamanho do índice se deve ao uso do Padrão de Bucket, em que um documento armazenará dados para muitos eventos, reduzindo o número total de documentos e, como consequência, reduzindo o número de entradas de índices.
Com essas melhorias significativas em relação ao tamanho do índice, é bem provável que também vejamos melhorias significativas no desempenho do aplicação . Um ponto de atenção nos valores apresentados acima é que o tamanho do índice das duas novas versões é menor que o tamanho da memória da máquina que executa o banco de dados, permitindo que todo o índice seja mantido no cache, o que é muito bom para um desempenho ponto de vista.

Carregar resultados de testes

Executando o teste de carga para appV5R0 e appV0R1 e plotando-o junto com os resultados para appV4, temos os seguintes resultados para Get Reports e Bulk Upsert:
Gráfico mostrando as taxas de appV4, appV5R0 e appV5R1 ao executar o teste de carga para a funcionalidade Obter Relatórios.
Gráfico mostrando as taxas de appV4, appV5R0 e appV5R1 ao executar o teste de carga para a funcionalidade Bulk Upsert. O gráfico acima mostra a melhor melhoria entre as versões que já tivemos nesta série. Ambas as novas versões, appV5R0 e appV0R1, são consideravelmente mais eficientes do que appV4, e appV5R1 é muito melhor do que appV5R0. Isso mostra que, em nosso caso, agrupar os dados por trimestre é melhor do que agrupar os dados por mês.
Para as Get Reports operações de, o appV5R0 aplicação conseguiu atingir pelo menos a taxa desejada mais baixa pela primeira vez, com appV5R1 quase atingindo todas as taxas desejadas no primeiro trimestre do teste de carga.
Para as Bulk Upsert operações de, ambas as novas versões quase atingiram as taxas desejadas durante todo o teste, com app5R0 queda nos últimos 20 minutos.

Problemas e melhorias

Como fizemos essa primeira implementação do a Bucket Pattern mais simples possível, algumas otimizações possíveis não foram consideradas. A principal é como lidamos com o items campo de array . Na implementação atual, apenas enviamos os documentos dos eventos para ele, mesmo quando já temos eventos para um dia específico.
Uma otimização clara aqui é aquela que estamos usando appV1 de para appV4, onde criamos apenas um documento por key e date/day, e quando temos muitos eventos para o mesmo key e date/dia, apenas incrementamos o status do documento com base no status do event.
Ao aplicar essa otimização, reduziremos o tamanho dos documentos porque a array de items terá menos elementos. Também reduziremos o custo computacional de geração dos relatórios porque estamos pré-calculando os totais de status por dia. Esse padrão de construção da pré-computação é bastante comum, pois tem seu próprio nome, Computed Pattern.

Versão do aplicativo 5 revisão 2 (appV5R2): usando o padrão de bucket com o padrão computado

Conforme discutido nas questões e melhorias de appV5R0 e appV5R1, podemos usar o Padrão Computado para pré-computar o status total por dia no items campo de array ao inserir um novo event. Isso reduz o custo de computação de gerar o reports e também reduz o tamanho do documento por ter menos elementos no items campo de array.
A maior parte dessa versão do aplicação será igual ao appV5R1, onde agrupamos os eventos por trimestre. A única diferença estará na Bulk Upsert operação, em que atualizaremos um elemento no items campo de array se um elemento com o mesmo date do novo event já existir ou inseriremos um novo elemento em items se um elemento com o mesmo date do novo não event existir.

Esquema

As implementações de aplicação apresentados acima teriam o seguinte esquema de documento Typescript SchemaV5R0 denominado:
1type SchemaV5R0 = {
2 _id: Buffer;
3 items: Array<{
4 date: Date;
5 a?: number;
6 n?: number;
7 p?: number;
8 r?: number;
9 }>;
10};

Upsert em massa

Com base na especificação apresentada, temos a seguinte operação em massa updateOne para cada event gerado pelo aplicação:
1const sumIfItemExists = itemsArray.buildResultIfItemExists(event);
2const returnItemsOrCreateNew = itemsArray.buildItemsOrCreateNew(event);
3const operation = {
4 updateOne: {
5 filter: {
6 _id: buildId(event.key, event.date),
7 },
8 update: [
9 { $set: { result: sumIfItemExists } },
10 { $set: { items: returnItemsOrCreateNew } },
11 { $unset: ['result'] },
12 ],
13 upsert: true,
14 },
15};
O código completo para esta updateOne operação é bastante grande, difícil de entender rapidamente, e também tornaria o processo de navegação pelo artigo um pouco trabalhoso. Por isso, aqui teremos um pseudocódigo disso. O código real que cria a operação pode ser encontrado no Github, e um exemplo de código completo pode ser encontrado neste repositório.
Nosso objetivo nessa operação de atualização é incrementar o status de um elemento na items array se já existir um element com o mesmo date do novo event ou criar um novo element se não há um com o mesmo date. Não é possível obter essa funcionalidade com os Operadores de Atualização do MongoDB . A solução é usar a Atualização com Aggregation Pipeline, que permite uma instrução de atualização mais expressiva.
Para facilitar a compreensão da lógica usada no pipeline, será fornecida uma versão JavaScript simplificada da funcionalidade.
O primeiro estágio, $set, definirá o campo result para a lógica da variável sumIfItemExists. Como o nome sugere, essa lógica iterará pela items array procurando elementos com o mesmo date que o event. Se houver um, este element terá o status presente no event somado/adicionado a ele. Como precisamos de uma maneira de acompanhar se um element com o mesmo date do event foi encontrado e o event status foi registrado, há uma variável booleana de ambiente chamada found que manterá o controle dele.
O código JavaScript a seguir apresenta uma funcionalidade semelhante ao que acontece no primeiro estágio do pipeline de agregação .
1const result = items.reduce(
2 (accumulator, element) => {
3 if (element.date === event.date) {
4 element.a += event.a;
5 element.n += event.n;
6 element.p += event.p;
7 element.r += event.r;
8
9 accumulator.found = true;
10 }
11
12 accumulator.items.push(element);
13
14 return accumulator;
15 },
16 { found: false, items: [] }
17);
A result variável/ campo será gerada usando um método de reduçãono items campo de array do documento que queremos atualizar. O valor inicial para o método de redução é um objeto com o campo found itemse. O campo accumulator.found tem um valor inicial de false e é responsável por sinalizar se um element na execução reduzida tinha o mesmo date que o que event queremos registrar. Se houver um element com a mesma data que event, element.date === event.date, os valores de status de element serão incrementados pelo status do event e o accumulator.found campo será definido como true, indicando que o event foi registrado. O accumulator.items campo de array terá o element de cada iteração enviado para ele, tornando-se o novo items campo de array.
O segundo estágio, $set, definirá o campo items para a lógica resultante da variável returnItemsOrCreateNew. Com um pouco de esforço de imagens, o nome sugere que a lógica presente na variável retornará o items campo do estágio anterior se um elemento com o mesmo date do event foi encontrado, found == true, ou retornar uma nova matriz gerada pela concatenação do campo items de matriz do estágio anterior com um novo campo de matriz contendo o event elemento quando um elemento com o mesmo date do event não foi encontrado durante a redução iterações, found == false.
O código JavaScript a seguir apresenta a lógica acima mais próxima do que acontece no agregação pipeline.
1let items = [];
2
3if (result.found == true) {
4 items = result.items;
5} else {
6 items = result.items.concat([event]);
7}
O terceiro estágio, $unset, removerá apenas o campo result que foi criado durante o primeiro estágio e usado no segundo estágio do pipeline.

Obter relatórios

serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para concluir a operaçãoGet Reports. Cada intervalo de data terá o seguinte pipeline, com apenas a data utilizada na função buildId sendo diferente:
1const pipeline = [
2 {
3 $match: {
4 _id: {
5 $gte: buildId(request.key, Date.now() - oneYear),
6 $lte: buildId(request.key, Date.now()),
7 },
8 },
9 },
10 {
11 $unwind: {
12 path: '$items',
13 },
14 },
15 {
16 $match: {
17 'items.date': { $gte: Date.now() - oneYear, $lt: Date.now() },
18 },
19 },
20 {
21 $group: {
22 _id: null,
23 approved: { $sum: '$items.a' },
24 noFunds: { $sum: '$items.n' },
25 pending: { $sum: '$items.p' },
26 rejected: { $sum: '$items.r' },
27 },
28 },
29 { $project: { _id: 0 } },
30];
Esse pipeline de agregação é o mesmo que appV5R0 e appV5R1. Uma explicação detalhada pode ser encontrada na seção anterior "Obter relatórios".

Índices

Como essa implementação usará o _id campo para suas operações, ela não precisará de um índice extra para dar suporte às operações Bulk Upsert Get Reports e.

Scenario

Inserindo os 500 milhões de documentos de evento para o cenário inicial nas collections appV5R2 com o esquema e a Bulk Upsert função apresentados acima, além de apresentar os valores das versões anteriores, temos o collection stats seguinte:
coleçãoDocumentostamanho de dadosTamanho do documentoTamanho do armazenamentoÍndicesTamanho do Índice
appV1359.639.62239,58GB119B8.78GB220.06GB
appV2359.614.53641.92GB126B10.46GB216.66GB
appV3359.633.37628.7GB86B8.96GB216.37GB
appV4359.615.27919.66GB59B6.69GB19.5GB
appV5R095.350.43119.19GB217B5.06GB12.95GB
appV5R133.429.46815.75GB506B4.04GB11.09GB
appV5R233.429.64911.96GB385B3.26GB11.16GB
Calculando o event stats para appV5R2 e também apresentam os valores das versões anteriores, temos o seguinte:
coleçãoTamanho dos Dados/ventosTamanho/ventos do ÍndiceTamanho total/ventos
appV185B43.1B128.1B
appV290B35.8B125.8B
appV361.6B35.2B96.8B
appV442.2B20.4B62.6B
appV5R041.2B6.3B47.5B
appV5R133.8B2.3B36.1B
appV5R225.7B2.5B28.2B
Analisando as tabelas acima, temos o resultado esperado apresentado na introdução, de appV5R1 a appV5R2. A única diferença notável é a 24redução de % no Data Size.
Essa redução no Data Size e Document size ajudará no desempenho de nosso aplicação , reduzindo o tempo gasto lendo o documento a partir do disco e o custo de processamento da descompactação do documento de seu estado compactado.

Carregar resultados de testes

Executando o teste de carga para appV5R2 e plotando-o juntamente com os resultados para appV5R1, temos os seguintes resultados para Get Reports e Bulk Upsert:
Gráfico mostrando as taxas de appV5R1 e appV5R2 ao executar o teste de carga para a funcionalidade Obter Relatórios. Gráfico mostrando as taxas de appV5R1 e appV5R2 ao executar o teste de carga para a funcionalidade Bulk Upsert. Os gráficos acima mostram uma pequena melhoria de desempenho no Get Reports e um pouco pior de desempenho no Bulk Upsert ao passar de appV5R1 para appV5R2. Reconheço que estava esperando mais.
Para entender por que obtivemos esse resultado, precisamos levar em consideração as taxas nas quais cada operação é executada e seu custo de processamento. A taxa média do Bulk Upsert é de 625 operações por segundo e do Get Reports é 137 de,5 operações por segundo. Com Bulk Upsert sendo executado em 4 média,5 vezes mais do que Get Reports, quando aumentamos sua complexidade usando o Computed Pattern, com o objetivo de diminuir o custo de computação de Get Reports, provavelmente mudamos seis para meia dezenas. O custo computacional geral provavelmente manteve-se inalterado.
Para aqueles que já se depararam com a definição do Computed Pattern na documentação MongoDB, o resultado anterior não foi uma grande novidade. Ali, temos, “”If reads are significantly more common than writes, the computed pattern reduces the frequency of data computation. Em nosso caso, as escritas são muito mais comuns do que as leituras, então o Computed Pattern pode não ser uma boa opção.

Problemas e melhorias

Vamos tentar extrair mais desempenho de nosso aplicação procurando melhorias em nossas operações atuais. Analisando o pipeline de agregação de Get Reports, encontramos um antipadrão muito comum quando campos de array estão envolvidos. Esse antipadrão é o $unwind seguido por um $match, que acontece no segundo e no terceiro estágios de nossa agregação pipeline.
Essa combinação de estágios pode prejudicar o desempenho do agregação pipeline porque estamos aumentando o número de documentos no pipeline com o estágio $unwind para, posteriormente, filtrar os documentos com $match o. Em outras palavras, para chegar a um estado final com menos documentos, passaremos por um estado intermediário em que aumentamos o número de documentos.
Na próxima revisão do aplicação , veremos como podemos alcançar o mesmo resultado final usando apenas um estágio e sem ter um estágio intermediário com mais documentos.

Versão do aplicativo 5 Revisão 3 (appV5R3): removendo um antipadrão do pipeline de agregação

Conforme apresentado nos problemas e melhorias de appV5R2, temos um antipadrão no agregação pipeline de Get Reports que pode reduzir o desempenho da query. Esse antipadrão écaracterizado por um $unwind estágio seguido por um $match. Essa combinação de etapas aumentará primeiro o número de documentos, $unwind, para posteriormente filtrá-los, $match. De forma simplificada, para chegar a um estado final, estamos passando por um estado intermediário caro.
Uma solução possível para contornar esse padrão é usar o $addFields estágio com o $filter operador no items campo de array. Com essa combinação, substituímos o items campo de array usando o $addFields estágio por um novo campo de array gerado pelo $filter operador na items array, onde filtramos todos os elementos onde date é dentro do intervalo de datas do relatório.
Mas, considerando nosso pipeline de agregação com a otimização apresentada acima, existe uma solução ainda melhor. Com o $filter operador, percorreremos todos os elementos no items campo e comparamos apenas suas datas com as datas do relatório para filtrar os elementos. Como o objetivo final de nossa query é obter os totais de status de todos os elementos dentro do intervalo de datas do relatório, em vez de apenas percorrer os elementos em items para filtrá-los, já poderíamos começar a calcular os totais de status. Podemos obter essa funcionalidade usando o $reduce operador em vez do $filter.

Esquema

As implementações de aplicação apresentados acima teriam o seguinte esquema de documento Typescript SchemaV5R0 denominado:
1type SchemaV5R0 = {
2 _id: Buffer;
3 items: Array<{
4 date: Date;
5 a?: number;
6 n?: number;
7 p?: number;
8 r?: number;
9 }>;
10};

Upsert em massa

Com base na especificação apresentada, temos a seguinte operação em massa updateOne para cada event gerado pelo aplicação:
1const sumIfItemExists = itemsArray.buildResultIfItemExists(event);
2const returnItemsOrCreateNew = itemsArray.buildItemsOrCreateNew(event);
3const operation = {
4 updateOne: {
5 filter: {
6 _id: buildId(event.key, event.date),
7 },
8 update: [
9 { $set: { result: sumIfItemExists } },
10 { $set: { items: returnItemsOrCreateNew } },
11 { $unset: ['result'] },
12 ],
13 upsert: true,
14 },
15};
Esta updateOne operação é igual ao appV5R2. Uma explicação detalhada pode ser encontrada na seção anterior "Upsert em massa".

Obter relatórios

serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para realizar a Get Reports operação:
1const pipeline = [
2 { $match: docsFromKeyBetweenDate },
3 { $addFields: itemsReduceAccumulator },
4 { $group: groupSumStatus },
5 { $project: { _id: 0 } },
6];
O código completo para esse pipeline de agregação é bastante complicado. Por isso, teremos apenas um pseudocódigo para isso aqui. O código real que cria a operação pode ser encontrado neste arquivo Typescript e um exemplo de código completo pode ser encontrado neste arquivo Markdown.
O primeiro estágio,, $match e o último estágio,, $project do agregação pipeline acima são iguais appV5R2 ao.
Conforme apresentado na introdução desta revisão, e considerando o nome da variável no segundo estágio, $addFields, ele adicionará um novo campo ao documento chamado totals que terá os totais de status para os elementos de items campo de array onde o date está no date intervalo do relatório. Este totals campo será gerado pelo $reduce operador que terá uma lógica equivalente ao seguinte código JavaScript:
1const totals = items.reduce(
2 (accumulator, element) => {
3 if (element.date > (Date.now() - oneYear) && element.date < Date.now()) {
4 accumulator.a += element.a;
5 accumulator.n += element.n;
6 accumulator.p += element.p;
7 accumulator.r += element.r;
8 }
9
10 return accumulator;
11 },
12 { a: 0, n: 0, p: 0, r: 0 }
13);
A reduce operação terá como valor inicial um documento/ objeto com os campos de status possíveis definidos 0 como. À medida que a função itera sobre os elementos de items, ela verificará se element date está na date faixa do relatório. Se o element date pertencer ao date intervalo do relatório, seus valores de status serão somados ao accumulator status.
O terceiro estágio, $group, terá uma lógica semelhante à que usamos até agora. A única diferença é que somaremos os valores presentes no campo totals em vez de items. O seguinte código é a lógica real usada no pipeline acima:
1const groupSumStatus = {
2 _id: null,
3 approved: { $sum: '$items.a' },
4 noFunds: { $sum: '$items.n' },
5 pending: { $sum: '$items.p' },
6 rejected: { $sum: '$items.r' },
7};

Scenario

Inserindo os 500 milhões de documentos de evento para o cenário inicial nas collections appV5R3 com o esquema e a Bulk Upsert função apresentados acima, além de apresentar os valores das versões anteriores, temos o collection stats seguinte:
coleçãoDocumentostamanho de dadosTamanho do documentoTamanho do armazenamentoÍndicesTamanho do Índice
appV1359.639.62239,58GB119B8.78GB220.06GB
appV2359.614.53641.92GB126B10.46GB216.66GB
appV3359.633.37628.7GB86B8.96GB216.37GB
appV4359.615.27919.66GB59B6.69GB19.5GB
appV5R095.350.43119.19GB217B5.06GB12.95GB
appV5R133.429.46815.75GB506B4.04GB11.09GB
appV5R233.429.64911.96GB385B3.26GB11.16GB
appV5R333.429.49211.96GB385B3,24GB11.11GB
Calculando o event stats para appV5R3 e também apresentam os valores das versões anteriores, temos o seguinte:
coleçãoTamanho dos Dados/ventosTamanho/ventos do ÍndiceTamanho total/ventos
appV185B43.1B128.1B
appV290B35.8B125.8B
appV361.6B35.2B96.8B
appV442.2B20.4B62.6B
appV5R041.2B6.3B47.5B
appV5R133.8B2.3B36.1B
appV5R225.7B2.5B28.2B
appV5R325.7B2.4B28.1B
Como o esquema do documento e as Bulk Upsert operações para appV5R3 são os mesmos que appV5R2, não há nada sobre o que raciocinar nesta seção entre as duas revisões.

Carregar resultados de testes

Executando o teste de carga para appV5R3 e plotando-o juntamente com os resultados para appV5R2, temos os seguintes resultados para Get Reports e Bulk Upsert:
Gráfico mostrando as taxas de appV5R2 e appV5R3 ao executar o teste de carga para a funcionalidade Obter Relatórios. Gráfico mostrando as taxas de appV5R2 e appV5R3 ao executar o teste de carga para a funcionalidade Bulk Upsert. Os gráficos acima mostram que temos outra revisão que não gerou ganhos claros. appV5R3 é apenas um pouco melhor do que appV5R2 em algumas partes de ambas as funções, Get Reports Bulk Upserte.
Do ponto de vista lógico, é claro que o pipeline de agregação de Get Reports de appV5R3 é mais otimizado e menos intensivo em CPU e memória do que appV5R2, mas não gerou resultados melhores. Isso pode ser explicado pelo gargalo atual da instância que executa o MongoDB, seu disco. Não entraremos em detalhes sobre isso para manter este artigo apenas grande, não enormes. Esse gargalo de disco ficará muito claro quando appV6R3 formos de para appV6R4 no próximo artigo.
Para aqueles que estão preocupados em saber como podemos verificar ou validar se o disco é o fator limitante em nossa instância, o gráfico de System CPU no Atlas Metrics tem a IOWAIT métrica, que mostra o quanto a CPU está esperando o disco. Nesta pasta do Github, você pode encontrar prints de algumas métricas para todos os testes de versão do aplicação e verificar as System CPU prints para appV5R2 e appV5R3 e ver a IOWAIT métrica atinge quase 15% do uso da CPU.

Problemas e melhorias

Acabamos de ver que a limitação da nossa implementação é o disco. Para resolver isso, temos duas opções: atualizar o disco onde o MongoDB armazena dados ou alterar nossa implementação para reduzir o uso do disco.
Como o objetivo desta série é mostrar quanto desempenho podemos obter com o mesmo hardware modelando como nosso aplicação armazena e lê dados do MongoDB, não atualizaremos o disco. Uma alteração na modelagem do aplicação para o MongoDB será deixada para o próximo artigo, appV6Rx.
appV5R4Para ,Computed Pattern duplicaremos o e pré-calcularemos os totais de status por trimestre, não apenas por dias. Embora saibamos que provavelmente não fornecerá melhor desempenho para os itens discutidos no "Resultado do teste de carga" de `appV5R2, vamos flexionar nosso conhecimento do MongoDB e do pipeline de agregação e também fornecer um exemplo de código de referência para os casos em que o Computed Pattern é um bom ajuste.

Versão do aplicativo 5 Revisão 4 (appV5R4): duplicando o padrão computado

Conforme apresentado nas edições e melhorias de appV5R3, para esta revisão, duplicaremos o mesmo Computed Pattern tendo boas pesquisas de que ele não proporcionará um desempenho melhor — mas, você sabe, para a ciência.
Também usaremos o Computed Pattern para pré-calcular os totais de status para cada documento. Como cada documento armazena os eventos por trimestre e usuário, nosso aplicação terá em cada documento os totais de status por trimestre e usuário. Esses totais pré-calculados serão armazenados em um campo chamado totals.
Um ponto de atenção nessa implementação é que estamos adicionando um novo campo ao documento, o que também aumentará o tamanho médio do documento . Conforme visto na revisão anterior, appV5R3, nosso gargalo atual é o disco, outra indicação de que esta implementação não terá melhor desempenho.

Esquema

As implementações de aplicação apresentados acima teriam o seguinte esquema de documento Typescript SchemaV5R1 denominado:
1type SchemaV5R1 = {
2 _id: Buffer;
3 totals: {
4 a?: number;
5 n?: number;
6 p?: number;
7 r?: number;
8 };
9 items: Array<{
10 date: Date;
11 a?: number;
12 n?: number;
13 p?: number;
14 r?: number;
15 }>;
16};

Upsert em massa

Com base na especificação apresentada, temos a seguinte operação em massa updateOne para cada event gerado pelo aplicação:
1const newReportFields = itemsArray.buildNewTotals(event);
2const sumIfItemExists = itemsArray.buildResultIfItemExists(event);
3const returnItemsOrCreateNew = itemsArray.buildItemsOrCreateNew(event);
4const operation = {
5 updateOne: {
6 filter: {
7 _id: buildId(event.key, event.date),
8 },
9 update: [
10 { $set: newReportFields },
11 { $set: { result: sumIfItemExists } },
12 { $set: { items: returnItemsOrCreateNew } },
13 { $unset: ['result'] },
14 ],
15 upsert: true,
16 },
17};
Conforme já explicado em outro código complicado que tornaria o artigo um pouco trabalhoso de ler, aqui temos apenas um pseudocódigo da implementação. O código que cria a operação pode ser encontrado neste arquivo Typescript , e um exemplo de código completo pode ser encontrado neste arquivo Markdown.
A única diferença entre a updateOne operação acima e a para appV5R3 é o primeiro estágio, $set. Este é o estágio em que pré-calcularemos o totals campo para o trimestre para o qual o documento armazena eventos. O seguinte código JavaScript apresenta uma funcionalidade semelhante ao que acontece no estágio:
1if (totals.a != null) totals.a += event.a;
2else totals.a = event.a;
3
4if (totals.n != null) totals.n += event.n;
5else totals.n = event.n;
6
7if (totals.p != null) totals.p += event.p;
8else totals.p = event.p;
9
10if (totals.r != null) totals.r += event.r;
11else totals.r = event.r;
Os últimos três estágios são estritamente iguais a, do appV5R3 qual você pode ver uma explicação detalhada na operação "Bulk upsert".

Obter relatórios

serão necessários cinco pipelines de agregação , um para cada intervalo de datas, para realizar a Get Reports operação:
1const pipeline = [
2 { $match: docsFromKeyBetweenDate },
3 { $addFields: itemsReduceAccumulator },
4 { $group: groupSumStatus },
5 { $project: { _id: 0 } },
6];
Conforme já explicado em outro código complicado que tornaria o artigo um pouco trabalhoso de ler, aqui temos apenas um pseudocódigo da implementação. O código que cria a operação pode ser encontrado neste arquivo Typescript , e um exemplo de código completo pode ser encontrado neste arquivo Markdown.
A maior parte da lógica do pipeline de agregação acima é igual à de appV5R3. Há uma pequena diferença: apenas uma if lógica, no segundo estágio, $addFields. Conforme visto em Get Reports explanation of appV5R3, neste estágio criaremos um novo campo chamado totals percorrendo os elementos do campo de array items e adicionando o status. Em nossa implementação atual, já temos o totals campo pré-computado no documento, portanto, se o intervalo de datas do trimestre estiver dentro dos limites do intervalo de datas do relatório, podemos usar o pré-computado .totals O seguinte código JavaScript apresenta uma funcionalidade semelhante ao que acontece no estágio:
1let totals;
2
3if (documentQuarterWithinReportDateRage) {
4 totals = document.totals;
5} else {
6 totals = document.items.reduce(
7 (accumulator, element) => {
8 if (element.date > Date.now() - oneYear && element.date < Date.now()) {
9 accumulator.a += element.a;
10 accumulator.n += element.n;
11 accumulator.p += element.p;
12 accumulator.r += element.r;
13 }
14
15 return accumulator;
16 },
17 { a: 0, n: 0, p: 0, r: 0 }
18 );
19}

Scenario

Inserindo os 500 milhões de documentos de evento para o cenário inicial nas collections appV5R4 com o esquema e a Bulk Upsert função apresentados acima, além de apresentar os valores das versões anteriores, temos o collection stats seguinte:
coleçãoDocumentostamanho de dadosTamanho do documentoTamanho do armazenamentoÍndicesTamanho do Índice
appV1359.639.62239,58GB119B8.78GB220.06GB
appV2359.614.53641.92GB126B10.46GB216.66GB
appV3359.633.37628.7GB86B8.96GB216.37GB
appV4359.615.27919.66GB59B6.69GB19.5GB
appV5R095.350.43119.19GB217B5.06GB12.95GB
appV5R133.429.46815.75GB506B4.04GB11.09GB
appV5R233.429.64911.96GB385B3.26GB11.16GB
appV5R333.429.49211.96GB385B3.24GB11.11GB
appV5R433.429.47012.88GB414B3.72GB11.24GB
Calculando o event stats para appV5R3 e também apresentam os valores das versões anteriores, temos o seguinte:
coleçãoTamanho dos Dados/ventosTamanho/ventos do ÍndiceTamanho total/ventos
appV185B43.1B128.1B
appV290B35.8B125.8B
appV361.6B35.2B96.8B
appV442.2B20.4B62.6B
appV5R041.2B6.3B47.5B
appV5R133.8B2.3B36.1B
appV5R225.7B2.5B28.2B
appV5R325.7B2.4B28.1B
appV5R427.7B2.7B30.4B
Conforme discutido nesta introdução de revisão, o totals campo adicional em cada documento na coleção aumentou o tamanho do documento e o tamanho geral do armazenamento. O Data Size de appV5R4 é,7 7% maior que appV5R3 e o Total Size/events é,8 2%. Como o disco é nosso fator limitante, o desempenho do appV5R4 provavelmente será pior do que appV5R3.

Carregar resultados de testes

Executando o teste de carga para appV5R4 e plotando-o juntamente com os resultados para appV5R3, temos os seguintes resultados para Get Reports e Bulk Upsert:
Gráfico mostrando as taxas de appV5R3 e appV5R4 ao executar o teste de carga para a funcionalidade Obter Relatórios. Gráfico mostrando as taxas de appV5R3 e appV5R4 ao executar o teste de carga para a funcionalidade Obter Relatórios. Os gráficos acima mostram o que espervamos desde o início desta revisão: o desempenho de appV5R4 é pior do que appV5R3 em ambas as funções, Get Reports e Bulk Upsert.

Problemas e melhorias

Conforme mostrado nos problemas e melhorias anteriores , para melhorar o desempenho do nosso aplicativo, precisamos alterar a implementação do MongoDB de uma forma que reduza o uso do disco. Para isso, precisamos reduzir o tamanho do documento .
Você pode pensar que não é possível reduzir o tamanho do documento e o tamanho geral da collection/índice ainda mais porque já estamos usando apenas um índice, concatenando dois campos em um, usando nomes de campo abreviados e usando Bucket Pattern o. Mas há uma coisa chamada Dynamic Schema que pode nos ajudar.
No Dynamic Schema, os valores de um campo se tornam nomes de campo . Assim, os nomes dos campo também armazenam dados e, como consequência, reduzem o tamanho do documento . Como esse padrão exigirá grandes alterações em nosso esquema atual do aplicação , iniciaremos uma nova versão, appV6Rx, com a qual trabalharemos na terceira parte desta série.

Conclusão

Esse é o fim da segunda parte da série. Cobrimos Bucket Pattern e Computed Pattern e as muitas maneiras de usar esses padrões para modelar como nosso aplicação armazena seus dados no MongoDB e os grandes ganhos de desempenho que eles podem fornecer quando usados corretamente.
Aqui está uma rápida revisão das melhorias feitas entre a versão do aplicação :
  • appV4 a appV5R0/appV5R1: essa é a implementação mais simples possível de Bucket Pattern, agrupando events por mês para appV5R0 e por trimestre para appV5R1.
  • appV5R1 a appV5R2: em vez de apenas enviar o event documento para a items array, passamos a pré-calcular os totais de status por dia, usando o Computed Pattern.
  • appV5R2 a appV5R3: isso melhorou o pipeline de agregação para Get Reports, evitando um estágio intermediário caro. Ela não forneceu melhorias de desempenho porque nossa instância do MongoDB está atualmente limitada por disco.
  • appV5R3 a appV5R4:Computed Pattern Dobramos para pré-calcular o totals campo, embora saibamos que o desempenho não seria melhor — mas apenas para a ciência.
Obtivemos melhorias notáveis na versão apresentada nesta segunda parte da série em comparação com as versões da primeira parte da série. appV0 a appV4. appV5R3 exibiu o melhor desempenho de todos, mas ainda não conseguiu atingir todas as taxas desejadas. Para a terceira e última versão desta série, as versões do nosso aplicação serão criadas em torno do Dynamic Schema Pattern, o que reduzirá o tamanho geral do documento e ajudará na limitação atual do disco.
Para ter mais dúvidas, você pode acessar o Fórum da MongoDB Community ou, se quiser criar seu aplicação usando o MongoDB, o MongoDB Developer Center tem muitos exemplos e tutoriais em diversas linguagens de programação.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.
Iniciar a conversa

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Artigo
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Dados da primavera desbloqueados: queries avançadas com MongoDB


Nov 08, 2024 | 7 min read
Tutorial

Segurança de tipo com Prisma e MongoDB


Aug 09, 2024 | 4 min read
Artigo

Queries que não diferenciam maiúsculas de minúsculas sem índices que não diferenciam maiúsculas de minúsculas


Oct 01, 2024 | 8 min read
exemplo de código

Gestão de revistas


Sep 11, 2024 | 0 min read
Sumário