MongoDB: Pipelines de agregação avançados
Última atualização: 2026-08-26
Agregação avançada de pipelines — O domínio de expressões complexas e conversões de tipos pode resolver 90% dos cenários de análise de dados.
Percurso de Aprendizagem Avançado para Pipelines de Agregação: Este curso começa com expressões condicionais ($cond/$switch/$ifNull), passando depois para operações com datas, conversões de tipos, operações com strings e operações com matrizes, culminando em um exercício prático abrangente. Cada tópico é independente, mas está interligado — as expressões condicionais são usadas para “determinar dias da semana/fins de semana” nas operações com datas, as conversões de tipo são usadas para “formatar datas com $toString” e as operações com strings são usadas para “concatenar e exibir texto”. Compreender essas conexões ajuda a construir uma rede de conhecimento abrangente.
O Sistema de Expressões do Pipeline de Agregação: As expressões no pipeline de agregação do MongoDB são divididas em cinco categorias: 1. Expressões booleanas ($cond/$switch/$ifNull/$and/$or/$not): verificações condicionais e combinações lógicas; 2. Expressões de comparação ($eq/$gt/$gte/$lt/$lte/$ne/$cmp): comparações de valores; 3. Expressões aritméticas ($add/$subtract/$multiply/$divide/$mod/$round): cálculos numéricos; 4. Expressões de string ($concat/$substr/$toUpper/$toLower/$split): processamento de texto; 5. Expressões de array ($map/$filter/$reduce/$arrayElemAt/$size): transformações de array. Ao dominar esses cinco tipos de expressões, você pode combiná-las para criar lógicas de transformação de dados de qualquer complexidade.
Mnemônico para classificar expressões: Os cinco tipos de expressões podem ser memorizados usando o seguinte processo: “Condicional → Comparação → Cálculo → Texto → Matriz” — 1. Primeiro, avalie a condição (booleana): É necessário um cálculo? Qual ramificação deve ser selecionada? 2. Em seguida, compare: o valor atende à condição? 3. Depois, calcule: execute operações aritméticas se a condição for atendida; 4. Em seguida, formate: converta o resultado do cálculo em texto para exibição; 5. Por fim, processe a coleção: execute operações em lote na matriz. Esse processo corresponde exatamente às etapas típicas do cálculo de relatórios — determinar condições de agrupamento → comparar limites → calcular valores estatísticos → formatar a saída → processar campos da matriz.
O poder da combinação de expressões: O verdadeiro poder do pipeline de agregação reside na combinação de expressões — enquanto uma única expressão realiza uma transformação simples, combiná-las pode resolver problemas complexos de negócios. Exemplo: Calcule o “Rótulo de Nível do Usuário” = $switch(condição: $gte(totalSpent, 10000) → 'VIP') → $concat(rótulo + totalSpent como string) → resultado “VIP ¥15.800”. Essa cadeia combina quatro tipos de expressões: $switch (condição), $gte (comparação), $concat (concatenação de strings) e $toString (conversão de tipos). Compreender a composição de expressões é a linha divisória entre “saber usar o pipeline de agregação” e “dominar o pipeline de agregação” — o primeiro conhece apenas operadores individuais, enquanto o segundo é capaz de combiná-los para construir qualquer pipeline de dados.
1. O que você vai aprender
- Usos avançados do acumulador $group
- Expressões condicionais: $cond / $ifNull / $switch
- Extração de data: $year / $month / $dayOfWeek
- Formatação de data $dateToString
- Conversão de tipos: $toString / $toInt / $toDecimal
- Operações com matrizes: $arrayElemAt / $map / $filter
graph LR
A[Document] -->|$cond<br/>Sanyuan| B[Conditional Projection]
A -->|$switch<br/>Multi-branch| C[Category Tags]
A -->|$ifNull<br/>Handling Null Values| D[Default Value Replacement]
A -->|$dateToString<br/>Date Format| E[Date Strings]
A -->|$toInt/$toDecimal<br/>Type Conversion| F[Type Conversion]
style B fill:#d4edda
style C fill:#d4edda
2. Expressões condicionais
Explicação do conceito: Uma expressão condicional funciona como o “fluxo de controle lógico” em um pipeline de agregação, permitindo que cada valor em um documento seja calculado dinamicamente com base em condições. $cond é uma expressão ternária (if-then-else), $switch é uma correspondência com múltiplas ramificações (semelhante a switch-case) e $ifNull é uma substituição de valor nulo (fornecendo um valor padrão). Elas são amplamente utilizadas em $project, $addFields e $group.
Árvore de decisão para seleção de expressões condicionais: A escolha da expressão condicional depende do cenário — 1. É necessário lidar apenas com valores nulos → $ifNull (a mais simples, por exemplo, {$ifNull: ['$nickname', '$username']}); 2. É necessária uma escolha do tipo if-else → $cond (por exemplo, {$cond: [{$gte: ['$age', 18]}, 'adult', 'minor']}); 3. São necessárias 3 ou mais ramificações → $switch (por exemplo, classificação por valor gasto: VIP/Prata/Bronze); 4. É necessário “retornar um valor padrão caso não seja encontrado” → $switch + default (semelhante ao CASE WHEN ... ELSE do SQL). Princípio de decisão: use $ifNull em vez de $cond sempre que possível; use $cond em vez de $switch sempre que possível — priorize a simplicidade e introduza complexidade apenas quando necessário.
Como funciona: $cond avalia uma condição “if” para cada documento e retorna o valor “then” ou “else”. É possível usar $cond aninhado para implementar condições em vários níveis, mas isso prejudica a legibilidade — nesses casos, usar $switch proporciona maior clareza. $ifNull Verifica se um campo é nulo ou indefinido; se for, ele o substitui por um valor alternativo. Todas as expressões condicionais são executadas individualmente para cada documento e não afetam outros documentos.
graph TB
A[Conditional Expression] --> B[$cond<br/>Sanyuan if-else]
A --> C[$switch<br/>Multi-branch matching]
A --> D[$ifNull<br/>Replacing Null Values]
B --> E["price >= 1000 → 'expensive'<br/>else price >= 100 → 'medium'<br/>else → 'cheap'"]
C --> F["status = 'paid' → 'Paid'<br/>status = 'shipped' → 'Shipped'<br/>default → 'Unknown'"]
D --> G["nickname is null<br/>→ Replace with username"]
style B fill:#d4edda
style C fill:#cce5ff
style D fill:#fff3cd
| Expressão | Sintaxe | Caso de uso | Legibilidade |
|---|---|---|---|
$cond |
{if, then, else} |
2 condições de ramificação | Médio |
Aninhado $cond |
Aninhado dentro de “else” $cond |
3 ou mais ramificações | Ruim |
$switch |
{branches, default} |
3 ou mais ramificações | Bom |
$ifNull |
{ $ifNull: [expr, default] } |
Tratamento de valores nulos | Bom |
Modelo computacional de expressões condicionais: As expressões condicionais ($cond/$switch/$ifNull) são avaliadas individualmente para cada documento dentro do pipeline de agregação — cada documento calcula seu próprio resultado condicional de forma independente, e documentos diferentes não afetam uns aos outros. Isso significa que $cond não interrompe os cálculos de outros documentos, e não há compartilhamento de estado entre documentos. Compreender isso ajuda a evitar erros comuns: não é possível fazer referência a campos de outros documentos dentro de um $cond; é necessário primeiro usar $lookup para estabelecer uma relação antes de realizar o cálculo.
Comparação de desempenho de expressões condicionais: Diferenças de desempenho entre as três expressões condicionais — 1. $ifNull: Mais rápida (verifica apenas se o valor é nulo/indefinido; comparação única); 2. $cond: Moderada (condição ternária; cada ramificação if/then/else executa a expressão uma vez); 3. $switch: Mais lenta (avalia os casos de cada ramificação sequencialmente até que seja encontrada uma correspondência). A diferença de desempenho é insignificante no nível de um único documento (na ordem de microssegundos), mas o efeito cumulativo é significativo em pipelines de agregação que processam milhões de documentos. Recomendações de otimização: 1. Use $ifNull em vez de $cond para substituições simples de valores nulos ({ifNull: ['$field', default]} vs {cond: [{eq: ['$field', null]}, default, '$field']}); 2. Classifique as ramificações de $switch por probabilidade de acerto (coloque o caso mais frequente em primeiro lugar para reduzir o número médio de avaliações); 3. Quando $cond estiver aninhado em mais de três níveis de profundidade, mude para o uso de $switch (que oferece melhor legibilidade e desempenho).
Princípios de funcionamento interno do acumulador $group: O acumulador $group ($sum/$avg/$min/$max/$push/$addToSet) mantém uma variável de estado na memória, que é atualizada sempre que um documento é processado. $sum começa em 0 e soma o valor do campo de cada documento; $avg mantém duas variáveis — soma e contagem — e calcula a média no final; $push acrescenta elementos a uma matriz; $addToSet acrescenta valores únicos. Entender como os acumuladores funcionam ajuda a projetar operações $group eficientes — evite usar $push dentro de $group para coletar grandes matrizes (o que consome muita memória); em vez disso, use $sum para contagem ou aplique $project primeiro para reduzir o número de campos.
Agregação x MapReduce: Em seus primórdios, o MongoDB utilizava o MapReduce para análises complexas de dados; o pipeline de agregação é uma alternativa mais moderna. Vantagens do pipeline de agregação: 1. Declarativo — basta descrever “o que fazer” em vez de “como fazer”, e o otimizador seleciona automaticamente o plano de execução; 2. Baseado em pipeline — as etapas são encadeadas, com cada uma focada exclusivamente em sua própria lógica; 3. Desempenho — os pipelines de agregação são implementados em C++, enquanto o MapReduce é interpretado e executado em JavaScript, resultando em uma diferença de desempenho de 10 a 100 vezes. O MongoDB 5.0 descontinuou o uso do MapReduce; o pipeline de agregação é a única ferramenta de análise recomendada.
Caminho de Aprendizagem Avançado para Pipelines de Agregação: O pipeline de agregação avançado abrange cinco categorias principais de operações: expressões condicionais ($cond/$switch/$ifNull), operações com datas ($year/$dateToString), conversões de tipo ($toString/$toInt/$toDecimal), operações com strings ($concat/$substr/$toUpper) e operações com matrizes ($arrayElemAt/$size/$map/$filter). Essas cinco categorias não são isoladas; elas estão interligadas nos pipelines reais — $group para agregação → $switch para ramificação condicional → $dateToString para formatação → saída. Dominar essas operações avançadas é a base para a criação de relatórios complexos.
Árvores de decisão para expressões condicionais: Como decidir qual expressão condicional usar — 1. Apenas verificar se há valores nulos? → $ifNull (a mais concisa, projetada especificamente para esse fim); 2. Apenas dois ramos? → $cond (a sintaxe de objeto {if, then, else} é mais legível; a sintaxe de array [condição, valor verdadeiro, valor falso] é mais compacta); 3. Três ou mais ramos? → $switch (compara os ramos um por um usando um array, com um valor padrão de fallback; a legibilidade é muito superior à de $cond aninhado); 4. Precisa verificar vários valores nulos de uma só vez? → Encadeie chamadas de $ifNull ($ifNull: [$a, $ifNull: [$b, 'padrão']]) ou use $switch + $eq: [null, '$field']. Escolher a expressão certa melhora significativamente a legibilidade do pipeline.
Comparação de desempenho de expressões condicionais: O desempenho das três expressões condicionais, da mais rápida à mais lenta, é: $ifNull > $cond > $switch. $ifNull é a mais rápida porque verifica apenas uma condição (nulo ou indefinido); $cond é um pouco mais lenta porque requer a avaliação de uma expressão if; $switch é a mais lenta porque precisa avaliar cada caso na matriz branches um por um. No entanto, a diferença de desempenho é normalmente inferior a 1 ms por mil documentos, e a legibilidade é mais importante do que o desempenho — um $cond aninhado com 100 ramificações é mais difícil de manter do que um $switch e, nesse caso, os benefícios de legibilidade do $switch superam em muito o custo de desempenho.
(1) O operador ternário $cond
Guia para a escolha de expressões condicionais: Critérios para selecionar entre os três tipos de expressões condicionais — $cond é adequado para condições com duas ramificações (por exemplo, “preço >= 1000 → caro/barato”); sua sintaxe é concisa, mas o aninhamento dificulta a leitura; Embora as instruções $cond aninhadas possam, teoricamente, suportar múltiplas ramificações, elas se tornam difíceis de manter além de três níveis; $switch é a melhor escolha para 3 ou mais ramificações (por exemplo, mapear o status do pedido para rótulos em chinês), já que a matriz branches é clara e fácil de ler, e default lida com casos não correspondentes; O $ifNull foi projetado especificamente para lidar com substituições de valores nulos (por exemplo, usar username quando nickname estiver vazio) e é uma ferramenta essencial para a programação defensiva.
// === Similar if-else ===
db.products.aggregate([
{
$project: {
title: 1,
priceLevel: {
$cond: {
if: { $gte: ['$price', 1000] },
then: 'expensive',
else: {
$cond: {
if: { $gte: ['$price', 100] },
then: 'medium',
else: 'cheap'
}
}
}
}
}
}
]);
(2) $ifNull: Tratamento de valores nulos
Casos de uso comuns para $ifNull: Existem três cenários típicos para o uso de $ifNull na análise de dados — 1. Substituição por valor padrão: $ifNull: ['$nickname', '$username'] Use o nome de usuário quando o nome de exibição não existir; 2. Proteção de cálculos: $ifNull: ['$discount', 0] Trate um desconto nulo como 0 (para evitar que o valor nulo seja incluído em operações aritméticas e resulte em um resultado nulo); 3. Marcação de dados ausentes: $ifNull: ['$lastLoginAt', 'never'] Marque os usuários que nunca fizeram login como “nunca”. O traço comum entre esses cenários é que null atua como um “buraco negro” para os dados — qualquer operação envolvendo null retorna null, e $ifNull é a única “via de fuga”.
Problema de propagação de nulo: A propagação de nulo nos pipelines de agregação do MongoDB é implícita e perigosa — se qualquer expressão tiver uma entrada nula, a expressão inteira retorna nulo. Por exemplo: $add: ['$price', '$tax']. Se tax for nulo, o resultado será nulo, em vez de price + 0. Isso não é um bug, mas sim o comportamento padrão do SQL (nulo + qualquer coisa = nulo); no entanto, na análise de dados, isso frequentemente faz com que uma coluna inteira de dados se torne nula. Estratégias de defesa: 1. Use $ifNull no estágio $project/$addFields para lidar com todos os campos que possam ser nulos; 2. Defina valores padrão ao inserir dados (por exemplo, tax: {type: Number, default: 0}); 3. Use $convert com onNull para lidar uniformemente com valores nulos durante a conversão de tipos.
Solução de problemas relacionados à propagação de valores nulos: Etapas para solucionar valores nulos inesperados nas saídas do pipeline de agregação — 1. Verifique etapa por etapa: adicione apenas um $addFields por vez para identificar em qual etapa o valor nulo aparece pela primeira vez; 2. Verifique os campos de origem: use $project para gerar os campos suspeitos separadamente (por exemplo, {tax: 1, price: 1}) e confirmar se o valor original é null; 3. Verifique a cadeia de cálculos: em um cálculo encadeado como $add → $multiply → $divide, se qualquer etapa for null, isso se propagará para todas as etapas subsequentes; 4. Prática de codificação defensiva: envolva todas as expressões aritméticas em $ifNull — por exemplo, $add: [{$ifNull: ['$price', 0]}, {$ifNull: ['$tax', 0]}]. Quando isso se tornar um hábito, problemas de propagação de nulo raramente ocorrerão.
// === Replace null/undefined ===
db.users.aggregate([
{
$project: {
name: 1,
displayName: {
$ifNull: ['$nickname', '$username'] // nickname Use when empty username
}
}
}
]);
(3) $switch: Ramificação com múltiplas condições
$switch: Ordem das ramificações e desempenho: O $switch avalia as ramificações na ordem em que são declaradas, e a primeira ramificação que corresponder retorna o resultado — portanto, você deve colocar a ramificação com maior probabilidade de corresponder em primeiro lugar para reduzir avaliações desnecessárias de condições. Em comparação com instruções $cond aninhadas, $switch oferece uma legibilidade significativamente melhor — quando há três ou mais ramificações, instruções $cond aninhadas resultam em recuos excessivamente profundos, enquanto a estrutura plana de $switch fica imediatamente clara. O ramo default em $switch não pode ser omitido — se nenhum ramo corresponder e não houver default, o MongoDB lançará um erro.
Aplicações do $switch na limpeza de dados: O $switch tem três usos típicos na limpeza de dados ETL — 1. Mapeamento de enumeração: conversão de códigos internos do banco de dados em rótulos legíveis (status: 'A' → 'Ativo', 'I' → 'Inativo', 'D' → 'Excluído'); 2. Agrupamento em faixas: discretização de valores contínuos (valor < 100 → 'Pequeno', 100–1000 → 'Médio', > 1000 → 'Grande'; semelhante à função $bucket, mas mais flexível, permitindo limites e rótulos personalizados); 3. Pontuação composta com múltiplas condições: atribuir uma classificação com base na combinação de vários campos (critérios VIP: gastos > 10.000 e cadastrado há > 1 ano; critérios Ouro: gastos > 1.000 ou cadastrado há > 3 anos; caso contrário, Bronze). A flexibilidade do $switch o torna uma ferramenta versátil para a transformação de dados.
Aplicações de expressões condicionais no ETL: As expressões condicionais são uma ferramenta essencial para a limpeza de dados no ETL — 1. Classificação de dados: $switch converte os status dos pedidos em rótulos comerciais (pendente → pagamento pendente, pago → pago, enviado → enviado); 2. Tratamento de exceções: $ifNull substitui campos ausentes por valores padrão, e $cond marca valores anormais como “requer revisão manual”; 3. Mascaramento de dados: $cond substitui os quatro dígitos do meio de um número de telefone por ** ({$concat: [{$substr: ['$phone', 0, 3]}, '**', {$substr: ['$phone', 7, 4]}]}); 4. Regras de negócios: $switch mapeia o valor gasto pelo usuário para um rótulo de nível. Em pipelines de ETL, as expressões condicionais são normalmente colocadas no estágio $addFields.
Como decidir qual expressão condicional usar: Escolhendo entre $cond, $switch e $ifNull — 1. Use $cond para uma escolha binária: quando houver apenas dois ramos (verdadeiro ou falso) (por exemplo, isVIP: {$cond: [{$gte: ['$spend', 10000]}, true, false]}); 2. Use $switch para múltiplos ramos: 3 ou mais ramos condicionais (por exemplo, determinação de nível, mapeamento de status); 3. Use $ifNull para tratamento de valores nulos: serve apenas para lidar com casos nulos/indefinidos (por exemplo, {$ifNull: ['$nickname', '$username']}); 4. Use $switch para condições aninhadas: evite aninhar $cond (mais de três níveis de aninhamento comprometem gravemente a legibilidade); em vez disso, use a estrutura plana de $switch. Regra geral — use $cond para duas ramificações, $switch para três ou mais ramificações e $ifNull para o tratamento de valores nulos.
// === Similar switch-case ===
db.orders.aggregate([
{
$project: {
orderId: '$_id',
statusLabel: {
$switch: {
branches: [
{ case: { $eq: ['$status', 'pending'] }, then: 'Payable' },
{ case: { $eq: ['$status', 'paid'] }, then: 'Paid' },
{ case: { $eq: ['$status', 'shipped'] }, then: 'Shipped' },
{ case: { $eq: ['$status', 'delivered'] }, then: 'Delivered' }
],
default: 'Unknown Status'
}
}
}
}
]);
3. Operações com datas
Explicação do conceito: As operações com datas constituem a base da análise de dados de séries temporais. O MongoDB oferece três tipos de operadores de data: (1) Operadores de extração ($year/$month/$dayOfMonth/$hour, etc.), que extraem componentes temporais de um campo Date; (2) Operadores de formatação ($dateToString), que convertem um campo Date em uma string no formato especificado; (3) Operadores aritméticos ($add/$subtract), que realizam operações de adição e subtração em datas.
Como funciona: O operador de extração lê os componentes correspondentes diretamente do tipo BSON Date e suporta o parâmetro timezone para lidar com fusos horários. $dateToString gera uma string usando especificadores de formato semelhantes aos do strftime. As operações com data são baseadas em carimbos de data/hora em milissegundos: $add soma um número de milissegundos, e $subtract calcula a diferença de tempo e, em seguida, divide-a por uma constante para convertê-la em dias ou horas.
Escolha de uma estratégia de fuso horário: Existem duas estratégias para lidar com fusos horários em operações com datas — 1. UTC uniforme na camada de armazenamento (recomendado): todas as datas são armazenadas em UTC e convertidas para a hora local usando o parâmetro timezone durante as consultas; 2. Fuso horário incluído na camada de armazenamento: cada documento registra um campo timezone, que é consultado durante as consultas. Vantagens da Estratégia 1: As condições de consulta não precisam levar em conta conversões de fuso horário, e as comparações de datas são diretas e precisas; consultas entre fusos horários não exigem conversões complexas de fuso horário. A Estratégia 2 é adequada para cenários de auditoria que exigem o registro preciso de “quando e em qual fuso horário um usuário realizou uma operação”.
Otimização do índice de datas: As consultas por intervalo de datas (como “pedidos dos últimos 7 dias”) são as consultas de séries temporais mais comuns. Pontos-chave de otimização: 1. O campo createdAt é indexado por padrão (timestamps: true + índice padrão); 2. Use $gte e $lt para consultas por intervalo de datas, em vez de $where ou agregações; 3. O índice composto {status: 1, createdAt: -1} abrange tanto a “filtragem por status” quanto a “classificação por data”; 4. $dateToString não utiliza o índice — ele converte Date em uma string antes de compará-lo. Você deve primeiro usar $match para verificar o intervalo de datas e, em seguida, usar $dateToString para formatação.
Padrões comuns para estatísticas agrupadas por data: O agrupamento por data é o cerne da análise de séries temporais — estatísticas agrupadas por dia, semana, mês, trimestre ou ano. Como definir chaves de agrupamento: 1. Agrupamento por mês: {_id: {year: {$year: '$date'}, month: {$month: '$date'}}}; 2. Agrupamento por semana: {_id: {year: {$year: '$date'}, week: {$week: '$date'}}}; 3. Agrupamento por dia: $dateToString: {format: '%Y-%m-%d', date: '$date'} como o _id. Observação: O método $dateToString não utiliza índices, mas sua sintaxe é a mais concisa. Para agrupamentos mensais e semanais, as funções de extração podem utilizar índices compostos para filtrar os dados antes do agrupamento.
Técnicas para preencher datas ausentes: Os resultados estatísticos agrupados por dia podem apresentar datas ausentes (por exemplo, se não houver pedidos em um determinado dia, os resultados não incluirão uma entrada para essa data), o que pode causar interrupções em um gráfico de linha contínua exibido pelo front-end — 1. Preenchimento na camada de aplicação: use o Node.js para percorrer o intervalo de datas e preencher as datas ausentes com o valor 0 (mais comum; código conciso); 2. Etapa $densify (MongoDB 6.1+): {$densify: {field: 'date', range: {step: 1, unit: 'day', bounds: 'full'}}} preenche automaticamente as datas ausentes (solução puramente de pipeline, sem necessidade de processamento na camada de aplicação); 3. Coleção auxiliar: crie uma coleção calendar para armazenar previamente todas as datas e, em seguida, use $lookup para preencher as datas ausentes por meio de uma junção (compatível com versões mais antigas, mas apresenta altos custos de manutenção). A opção 2 é a mais elegante, mas requer o MongoDB 6.1 ou posterior; a opção 1 é a mais versátil.
Melhores práticas para consultas de intervalo de datas: A maneira correta de consultar “os últimos N dias” — 1. Use $gte + new Date(Date.now() - N86400000) (calculado em JavaScript, passe o objeto Date para o MongoDB); 2. Use $gte + ISODate() (sintaxe do Mongo Shell); 3. Use $match + $expr + $gte: ['$createdAt', {$subtract: ['$$NOW', N86400000]}] no pipeline de agregação (expressão pura do pipeline; $$NOW refere-se à hora atual do servidor). A opção 3 é a mais flexível — ela não depende de cálculos de tempo no nível do aplicativo e é autônoma dentro do pipeline.
graph LR
A[Date Field<br/>2026-07-01T10:30:00Z] --> B[$year → 2026]
A --> C[$month → 7]
A --> D[$dayOfMonth → 1]
A --> E[$hour → 10]
A --> F["$dateToString<br/>%Y-%m-%d → '2026-07-01'"]
A --> G["$add[date, 7*86400000]<br/>→ 2026-07-08"]
A --> H["$subtract[now, date]<br/>→ Difference in the number of days"]
style B fill:#d4edda
style F fill:#cce5ff
style G fill:#fff3cd
Problemas de precisão nos cálculos de datas: $subtract Ao calcular a diferença entre datas, o resultado é retornado em milissegundos. Dividir esse valor por 86.400.000 para convertê-lo em dias pode causar problemas de precisão — as mudanças no horário de verão podem fazer com que um dia tenha menos do que exatamente 24 horas. Para cálculos precisos, use $dateToString para extrair a parte da data e compará-la, ou use a diferença em $dayOfYear para obter uma aproximação. Na maioria dos cenários empresariais, a precisão em milissegundos é suficiente, mas é necessário prestar atenção especial a sistemas financeiros e de controle de ponto.
Erros comuns na manipulação de datas: 1. Confusão com fusos horários — armazenar dados em UTC, mas esquecer de convertê-los ao exibir a hora local; 2. Os meses começam em 1, mas o JavaScript Date começa em 0 (o $month do MongoDB retorna 1–12, o que difere do JavaScript); 3. $dayOfWeek retorna 1 para domingo (convenção dos EUA; usuários chineses podem esperar que 1 seja segunda-feira); 4. $week retorna o número da semana no ano, mas diferentes países têm definições diferentes sobre em que dia a semana começa (a ISO 8601 especifica segunda-feira como o início da semana, enquanto o MongoDB usa domingo como padrão); 5. $add, ao somar milissegundos, não leva em conta os segundos bissextos (isso não tem impacto na grande maioria das aplicações).
Otimização de desempenho para operações com data: As operações com data não utilizam índices no pipeline de agregação — funções de extração como $year e $month primeiro convertem a data em um valor numérico antes de realizar comparações, portanto, não podem utilizar o índice B-tree no campo Date. Portanto: 1. Para consultas por intervalo de datas, use $gte/$lt (que compara diretamente no campo Date e utiliza o índice) em vez de $month === 7; 2. Ao agrupar por mês, filtre usando {$gte: startOfMonth, $lt: startOfNextMonth} antes de chamar $group; 3. Use $dateToString apenas durante a etapa final de saída (para formatação), e não durante a etapa de filtragem.
Resumo das melhores práticas para datas: As regras de ouro para operações com datas no MongoDB — 1. Sempre armazene as datas em UTC (para evitar confusão de fuso horário; converta para o fuso horário desejado na camada de aplicação ou usando o parâmetro timezone em $dateToString ao exibir); 2. Sempre use $gte/$lt para consultas (para utilizar índices; evite filtrar com funções de extração como $month ou $year); 3. Use $dateToString ou $dateFromParts para construir chaves de agrupamento (por exemplo, "$dateToString: {format: '%Y-%m', date: '$createdAt'}" para agrupar por mês); 4. Realize a formatação apenas na camada de saída (como etapa final em $project/$addFields); 5. Especifique o fuso horário no parâmetro timezone de $dateToString (formato IANA, por exemplo, 'Asia/Shanghai'); não altere manualmente o valor da hora na camada de aplicação (o horário de verão fará com que o deslocamento mude).
(1) Extração de data
Estratégia de tratamento de fuso horário: O MongoDB armazena datas como carimbos de data e hora em UTC (sem informações de fuso horário), e os operadores de extração de data retornam valores em UTC por padrão. Para cenários que exigem a hora local: 1. O parâmetro timezone de $dateToString (por exemplo, 'Asia/Tokyo') pode gerar diretamente uma string com a hora local; 2. Operadores de extração como $hour e $month também suportam o parâmetro timezone; 3. Realizar a conversão de fuso horário na camada de aplicação oferece maior flexibilidade, mas aumenta a quantidade de código. Recomendação para produção: armazene os dados uniformemente em UTC e realize a conversão usando $dateToString ou na camada de aplicação ao exibi-los.
Armadilhas comuns no tratamento de fusos horários: Os fusos horários são a área mais propensa a erros nos pipelines de agregação — 1. Armadilha do horário de verão (DST): Os EUA e a Europa adotam o horário de verão, portanto, o deslocamento em relação ao UTC para a mesma cidade varia de acordo com o mês (Nova York fica em UTC-5 durante o horário padrão e em UTC-4 durante o horário de verão). O parâmetro timezone do MongoDB lida automaticamente com o horário de verão, mas cálculos manuais de deslocamento podem levar a erros; 2. Armadilha do agrupamento entre dias: 02h00 do dia 1º de junho de 2026 (UTC+8) corresponde às 18h00 do dia 31 de maio de 2026 (UTC). Usar $dayOfMonth sem especificar um fuso horário agrupará a data como 31, em vez de 1; 3. Armadilhas do segundo bissexto/ano bissexto: ao adicionar um mês com $dateAdd, 31 de janeiro + 1 mês = 28 de fevereiro (não 3 de março); o MongoDB lida com isso automaticamente; 4. Atualizações do banco de dados de fusos horários: o MongoDB inclui um banco de dados de fusos horários da IANA integrado, mas versões mais antigas podem não conter as regras mais recentes de fusos horários; portanto, é necessário atualizar o MongoDB regularmente.
Melhores práticas para operações com datas: 1. Use $gte/$lt para consultas de intervalo de datas em vez de funções de extração como $dayOfMonth (as primeiras podem utilizar índices, enquanto as últimas não); 2. Ao agrupar por mês, use {_id: {year: {$year: '$date'}, month: {$month: '$date'}}} em vez de $dateToString (o primeiro pode utilizar índices); 3. Cálculos de data (soma ou subtração de dias) são mais eficientes no pipeline de agregação do que na camada de aplicação (para evitar a transferência de grandes quantidades de campos de data).
// === $year / $month / $dayOfWeek / $hour ===
db.orders.aggregate([
{
$project: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' },
day: { $dayOfMonth: '$createdAt' },
weekday: { $dayOfWeek: '$createdAt' }, // 1=Sunday
hour: { $hour: '$createdAt' }
}
}
]);
(2) Formatação de datas
Referência rápida para os especificadores de formato do $dateToString: O $dateToString utiliza especificadores de formato no estilo strftime — %Y (ano de quatro dígitos), %m (mês de dois dígitos), %d (dia de dois dígitos), %H (hora no formato de 24 horas), %M (minutos), %S (segundos), %L (milissegundos), %j (dia do ano), %U (semana do ano). Combinações comuns: '%Y-%m-%d' (chave de data), '%Y-%m' (chave de mês), '%Y-W%U' (chave de semana), '%Y-%m-%d %H:00' (chave de hora). O parâmetro de fuso horário aceita o formato Olsen (por exemplo, 'Asia/Shanghai') ou um deslocamento em relação ao UTC (por exemplo, '+08:00').
Aplicações típicas da formatação de datas: O uso mais comum da formatação de datas é para agrupar estatísticas por período — utilizando $dateToString para gerar chaves de data e, em seguida, utilizando $group para agregar dados com base nessas chaves. Por exemplo: acompanhar tendências de vendas por mês ($dateToString → $group → $sort), identificar picos de tráfego por hora e medir a retenção de usuários por semana. Otimização de chaves: Se você precisar apenas agrupar por mês, usar {year: {$year}, month: {$month}} é mais eficiente do que $dateToString (o primeiro pode utilizar índices, enquanto o segundo converte a data em uma string e não pode usar índices).
// === $dateToString Date Format ===
db.orders.aggregate([
{
$project: {
orderDate: {
$dateToString: {
format: '%Y-%m-%d %H:%M:%S',
date: '$createdAt',
timezone: 'Asia/Tokyo'
}
}
}
}
]);
// { orderDate: '2026-07-01 10:30:00' }
| Especificador de formato | Significado |
|---|---|
%Y |
ano de 4 dígitos |
%m |
Mês com 2 dígitos |
%d |
data de 2 dígitos |
%H |
Relógio de 24 horas |
%M |
Ata |
%S |
Segundos |
(3) Operações com datas
Cenários de negócios para cálculos de datas: Os cálculos de datas são extremamente comuns em sistemas de comércio eletrônico e SaaS — 1. Verificações de validade de pedidos: $add[createdAt, 3086400000] calcula o prazo final de pagamento; os pedidos são automaticamente cancelados se essa data for ultrapassada; 2. Análise de atividade do usuário: $subtract[now, lastLoginAt] calcula o número de dias desde o último login; usuários com mais de 30 dias desde o último login são marcados como inativos; 3. Lembretes de renovação de assinatura: $subtract[expiryDate, now] calcula o número de dias restantes; se for <7 dias, um lembrete é enviado; 4. Janelas de tempo para relatórios: $match {createdAt: {$gte: $add[now, -3086400000]}} recupera dados dos últimos 30 dias.
Conversão de unidades de tempo para operações com datas: A unidade subjacente para operações com datas no MongoDB é o milissegundo — 1 dia = 86.400.000 milissegundos, 1 hora = 3.600.000 milissegundos. Erros comuns de conversão: 1. Esquecer de multiplicar por 1.000 (usar 86.400 em vez de 86.400.000), resultando em uma diferença de 1.000 vezes; 2. Usar 30*86.400.000 para representar “30 dias” é uma aproximação (já que o número de dias varia de acordo com o mês); para cálculos precisos de data, use $dateAdd (MongoDB 5.0+) em vez de $add; 3. Problema de fuso horário: operadores como $hour e $dayOfMonth usam UTC como padrão; para usar a hora local, adicione o parâmetro timezone (por exemplo, {$hour: {date: '$createdAt', timezone: 'Asia/Shanghai'}}). Cálculos de tempo são uma fonte comum de bugs — certifique-se de validar a lógica crítica de datas com testes unitários.
// === $add / $subtract Date Addition and Subtraction ===
db.orders.aggregate([
{
$project: {
createdAt: 1,
expiryDate: { $add: ['$createdAt', 7 * 24 * 60 * 60 * 1000] }, // Add 7 days
daysSinceCreated: {
$divide: [
{ $subtract: [new Date(), '$createdAt'] },
1000 * 60 * 60 * 24
]
}
}
}
]);
4. Conversão de tipos
Explicação do conceito: O MongoDB é um banco de dados de tipagem fraca; os campos dentro da mesma coleção podem ter tipos de dados diferentes (por exemplo, price pode ser um String ou um Number). O pipeline de agregação oferece operadores de conversão, como $toString/$toInt/$toLong/$toDouble/$toDecimal/$toDate/$toBool, para resolver inconsistências de tipo de dados. $convert oferece um método de conversão mais seguro (permitindo que você especifique o tratamento de erros com onError).
Como funciona: O operador de conversão de tipos realiza conversões nos valores dos campos de cada documento. $toInt('42') → 42, $toString(3.14) → '3,14'. Se a conversão falhar, $toXxx retorna nulo e gera um aviso; $convert pode ser usado para especificar que onError retorne um valor personalizado. Use conversões em $project ou $addFields para garantir a consistência dos tipos de dados nas etapas subsequentes.
Desafios dos sistemas com tipagem fraca: A tipagem fraca do MongoDB é uma faca de dois gumes — oferece flexibilidade (não é preciso predefinir tipos), mas também apresenta riscos (o mesmo campo pode conter diferentes tipos de dados). Problemas típicos: 1. Ao migrar dados legados, price pode ser a string “599” ou o número 599; 2. Quando $avg em $group encontra um campo de string, ele retorna null em vez de gerar um erro; 3. A ordenação de resultados com tipos mistos em $sort é imprevisível (a ordem é determinada pela comparação de tipos BSON). Solução: Use $convert na primeira etapa do pipeline de agregação para padronizar os tipos ou aplique rigorosamente as restrições de tipo no nível do esquema do Mongoose.
Modo de conversão segura do $convert: O $convert é mais seguro do que o $toXxx — ele suporta os parâmetros onError e onNull, permitindo que você retorne um valor padrão personalizado em vez de null caso a conversão falhe. Padrão recomendado: $convert({input: '$price', to: 'decimal', onError: NumberDecimal('0'), onNull: NumberDecimal('0')}). Dessa forma, mesmo que price seja uma string não numérica ou null, o pipeline de agregação pode continuar sendo executado sem causar problemas de propagação de null. Pipelines de agregação em produção devem sempre usar $convert em vez de $toXxx.
Resumo das melhores práticas para conversão de tipos: 1. Sempre use $convert + onError em vez de apenas $toXxx (para tratamento de erros); 2. Padronize os tipos logo no início do pipeline ($addFields + $convert) para garantir que os tipos de dados permaneçam consistentes ao longo das etapas subsequentes; 3. Use $type para verificar os tipos de campo e aplicar diferentes lógicas de conversão para diferentes tipos ($switch + $type); 4. Use Decimal128 para cálculos monetários, Double para cálculos científicos e Int32 para contagem; 5. Converta datas em formato de string para Date usando $toDate (formato ISO 8601); para formatos não padronizados, use $dateFromString primeiro ou realize um pré-processamento na camada de aplicação.
Dicas para depurar conversões de tipo: Depurar erros de tipo em pipelines de agregação é a tarefa que mais consome tempo — 1. Use o operador $type para verificar os tipos dos campos: $addFields: {priceType: {$type: '$price'}} para ver qual é o tipo do campo price em cada documento; 2. Use $cond + $type para conversões condicionais: $switch: {branches: [{case: {$eq: [{$type: '$price'}, 'string']}, then: {$toDecimal: '$price'}}, {case: {$eq: [{$type: '$price'}, 'double']}, then: {$toDecimal: '$price'}}], default: '$price'}; 3. Visualize a saída passo a passo no Compass para localizar onde ocorre a incompatibilidade de tipos. É melhor prevenir do que remediar — aplique rigorosamente as restrições de tipo no esquema do Mongoose para evitar problemas de tipagem fraca na fonte.
| Operador de conversão | Entrada → Saída | Cenários típicos |
|---|---|---|
$toString |
Qualquer → String | Converte valores numéricos em strings para exibição |
$toInt |
String/Número→Int32 | Converte o preço em string em um número inteiro |
$toLong |
String/Número → Int64 | Conversão de ID de números grandes |
$toDouble |
String/Número → Double | Cálculo exato |
$toDecimal |
String/Número→Decimal128 | Cálculos precisos de moeda |
$toDate |
String/Número → Data | Data em formato de string para Data |
$convert |
É possível especificar onError | Conversão segura (recomendada) |
Diretrizes de segurança para conversão de tipos: A natureza de tipagem fraca do MongoDB significa que um único campo pode conter vários tipos — por exemplo, price poderia ser um String, um Number ou até mesmo um Decimal128. Os operadores de conversão de tipos resolvem essa inconsistência, mas as seguintes diretrizes de segurança devem ser observadas: 1. Conversões estritas, como $toInt e $toDouble, gerarão um erro imediatamente ao encontrar um valor incompatível; 2. $convert, combinado com onError e onNull, oferece conversão tolerante a falhas (recomendado); 3. Use $type para verificar o tipo do campo antes da conversão, a fim de evitar conversões cegas; 4. A limpeza de dados deve ser concluída durante a fase de ETL; as conversões no pipeline de agregação devem servir como último recurso.
Comparação entre $convert e $toXxx:
| Dimensão | $toXxx | $convert |
|---|---|---|
| Sintaxe | Simples | Um pouco complexa |
| Tratamento de erros | Interrupção por erro | onError retorna um valor personalizado |
| Tratamento de valores nulos | Retornar nulo | onNull retorna um valor personalizado |
| Cenários recomendados | Dados com integridade comprovada | Tolerância a falhas em ambientes de produção |
Armadilhas comuns na conversão de tipos: 1. $toInt('3.14') gera um erro — é preciso usar primeiro $toDouble e, em seguida, $toInt; 2. $toDate('2026-07-01') é bem-sucedido, mas $toDate('07/01/2026') falha — o MongoDB reconhece apenas o formato ISO 8601; 3. $toBool('false') retorna true — strings não vazias são consideradas verdadeiras; 4. $toDouble(null) retorna null em vez de 0 — a propagação de null em operações aritméticas subsequentes faz com que toda a expressão seja avaliada como null. Estratégia geral para evitar essas armadilhas: primeiro use $ifNull para lidar com valores nulos e, em seguida, use $convert para lidar com as conversões de tipo.
// === Numeric Type Conversion ===
db.products.aggregate([
{
$project: {
title: 1,
price: 1,
priceString: { $toString: '$price' }, // Decimal128 → string
priceInt: { $toInt: '$price' }, // → Int32
priceLong: { $toLong: '$price' }, // → Int64
priceDouble: { $toDouble: '$price' }, // → Double
priceDecimal: { $toDecimal: '$price' } // → Decimal128
}
}
]);
// === Date Conversion ===
db.products.aggregate([
{
$project: {
title: 1,
releaseDate: { $toDate: '$releaseDateStr' } // string → Date
}
}
]);
5. Operações com cadeias de caracteres
Considerações de desempenho para operações com strings: As operações com strings no pipeline de agregação são executadas documento a documento, o que pode se tornar um gargalo ao processar grandes conjuntos de dados. Pontos importantes a serem observados: 1. $substr trunca por bytes; caracteres multibyte UTF-8, como caracteres chineses, podem ser truncados — certifique-se de usar $substrCP para truncar por caracteres; 2. Ao concatenar um grande número de campos com $concat, preste atenção ao comprimento resultante (cadeias BSON são limitadas a 16 MB); 3. $regexMatch pode ser usado com $filter para implementar filtros de correspondência aproximada; 4. As operações com cadeias de caracteres devem ser realizadas após $match (filtre primeiro para reduzir a carga de processamento).
Armadilhas comuns na manipulação de strings: 1. $concat e null: $concat retorna null se qualquer parâmetro for null — envolva cada parâmetro em $ifNull ({$concat: [{$ifNull: ['$firstName', '']}, ' ', {$ifNull: ['$lastName', '']}]}); 2. $toLower/$toUpper e suporte multilíngue: Esses operadores lidam apenas com caracteres ASCII; chinês, japonês e coreano não são afetados (já que não fazem distinção entre maiúsculas e minúsculas), mas a conversão do caractere alemão ß para SS não é suportada; 3. $split e strings vazias: $split: ['', ','] retorna um array vazio [] (em vez de ['']), o que difere do comportamento de ''.split(',') no JavaScript; 4. $replaceOne vs. $replaceAll: O MongoDB 4.4+ usa $replaceOne (substitui a primeira ocorrência) e $replaceAll (substitui todas as ocorrências). Observe que esses operadores não estão disponíveis em versões mais antigas.
A principal diferença entre $substr e $substrCP: $substr extrai texto com base em deslocamentos de bytes, enquanto $substrCP extrai texto com base em deslocamentos de caracteres — os dois são equivalentes para texto ASCII puro, mas, como caracteres CJK, emojis e letras acentuadas são caracteres multibyte, $substr pode truncar metade de um caractere e resultar em texto ilegível. Por exemplo: para “café”, ao usar $substr: ['$text', 0, 4] para extrair os primeiros 4 bytes, o resultado é um texto ilegível (já que “é” ocupa 2 bytes em UTF-8, $substr o dividiria ao meio); ao usar $substrCP: ['$text', 0, 4] para extrair os primeiros 4 caracteres, o resultado correto é “café”. Regra: Se um campo puder conter caracteres não ASCII, use sempre $substrCP.
Como usar $regexMatch: $regexMatch realiza a correspondência de expressões regulares no pipeline de agregação — ele retorna um valor booleano e é frequentemente usado em combinação com $filter para filtrar elementos de um array. Por exemplo: filtrar tags em um array que comecem com “mongo” — $filter: {input: '$tags', cond: {$regexMatch: {input: '$$this', regex: '^mongo'}}}}. Observação: o $regexMatch não utiliza índices — ele compara cada valor individualmente, e o desempenho varia linearmente com a quantidade de dados. Para pesquisas com expressões regulares em grandes conjuntos de dados, use primeiro o $match (que pode utilizar índices), seguido pelo $regexMatch.
Tratamento de valores NULL no $concat: Quando o $concat encontra uma entrada NULL, toda a expressão retorna NULL — isso não é um bug, mas sim o comportamento padrão de propagação de NULL no SQL. Por exemplo: $concat: ['$firstName', ' ', '$lastName']. Se lastName for NULL, o resultado completo será NULL, em vez de “John NULL”. Solução: 1. Envolva cada campo que possa ser nulo em $ifNull ($concat: [$ifNull: ['$firstName', ''], ' ', $ifNull: ['$lastName', '']]); 2. Substitua a conversão implícita de $concat por $convert + onError; 3. Certifique-se de que os campos de string tenham um valor padrão (padrão: '') ao inserir dados no banco de dados. A propagação de valores nulos é a causa mais comum de “resultados nulos inesperados” em operações com strings.
Combinando $split e $arrayElemAt: $split divide uma string em um array com base em um delimitador, e $arrayElemAt recupera um elemento pelo seu índice — combinar os dois permite “extrair uma parte específica de uma string”. Cenários típicos: 1. Extrair o domínio de um endereço de e-mail — $arrayElemAt: [$split: ['$email', '@'], 1] recupera a parte após o símbolo @; 2. Extrair um nome — $arrayElemAt: [$split: ['$fullName', ' '], 0] recupera a parte antes do espaço; 3. Analisar um caminho — $arrayElemAt: [$split: ['$path', '/'], -1] recupera o último segmento do caminho. Observe que $split retorna null para entradas nulas (requer proteção com $ifNull) e retorna [''] para uma string vazia (o array contém uma única string vazia, não um array vazio).
Tratamento de valores nulos com $concat: Uma armadilha crítica do $concat — se qualquer entrada for nula, o resultado inteiro retorna nulo. Por exemplo: $concat: ['$firstName', ' ', '$lastName']. Se lastName for nulo, o resultado será nulo, em vez de “John nulo”. Solução alternativa: Coloque cada entrada que possa ser nula entre $ifNull—$concat: [$ifNull: ['$firstName', ''], ' ', $ifNull: ['$lastName', '']]. Esse padrão é extremamente comum ao concatenar campos como endereços e nomes.
Combinando $split e $arrayElemAt: $split divide uma string em um array, e $arrayElemAt recupera o elemento no índice especificado — combinar os dois permite “extrair subcadeias”. Exemplos clássicos: 1. Extrair o domínio de um endereço de e-mail: $arrayElemAt: [{ $split: ['$email', '@'] }, 1] → “gmail.com”; 2. Extrair o caminho de uma URL: $arrayElemAt: [{ $split: ['$url', '/'], 3 }]; 3. Extrair o sobrenome de um nome completo: $arrayElemAt: [{ $split: ['$fullName', ' '] }, 0]. $split não suporta delimitadores de expressões regulares — se o delimitador não for fixo, você deve primeiro usar $trim para remover espaços extras.
// === $substr String Truncation ===
db.users.aggregate([
{
$project: {
email: 1,
emailPrefix: { $substr: ['$email', 0, 5] } // First 5 characters of email
}
}
]);
// === $concat String Concatenation ===
db.users.aggregate([
{
$project: {
fullName: { $concat: ['$firstName', ' ', '$lastName'] }
}
}
]);
// === $toUpper / $toLower Uppercase and lowercase ===
db.users.aggregate([
{
$project: {
usernameUpper: { $toUpper: '$username' }
}
}
]);
6. Operações com matrizes
O papel das operações com matrizes no pipeline de agregação: As operações com matrizes são a principal característica que distingue o pipeline de agregação do MongoDB do SQL — as operações em nível de linha do SQL não conseguem lidar com matrizes aninhadas, enquanto $map, $filter e $reduce permitem transformar, filtrar e agregar elementos dentro de matrizes. Isso decorre diretamente da natureza de matrizes aninhadas do modelo de documento do MongoDB. $map equivale a aplicar $project a cada elemento de um array, $filter equivale a aplicar $match ao array e $reduce equivale a aplicar $group ao array — dominar esses três operadores significa dominar os “recursos de agregação” no nível do array.
Riscos e soluções alternativas para $unwind: $unwind divide um array em vários documentos, sendo que cada documento contém um elemento do array. Riscos: 1. Dividir um array grande gera um grande número de documentos (por exemplo, um array de 1.000 elementos → 1.000 documentos), causando um aumento excessivo de dados no pipeline; 2. Após a divisão, é necessário usar $group para reagrupar os dados, o que aumenta a complexidade; 3. Por padrão, matrizes vazias resultam no descarte do documento inteiro (requer preserveNullAndEmptyArrays: true). Melhores práticas: Evite usar $unwind se o problema puder ser resolvido com $map ou $filter; quando $unwind for necessário, utilize imediatamente $group em seguida para reorganizar os dados.
Correspondência entre $map, $filter e $reduce e os métodos de array do JavaScript: Os operadores de array no pipeline de agregação correspondem, um a um, aos métodos de array do JavaScript — $map ↔ Array.map() (transformar cada elemento), $filter ↔ Array.filter() (filtrar elementos), $reduce ↔ Array.reduce() (cálculo acumulativo), $concatArrays ↔ [...a, ...b] (mesclar arrays), $reverseArray ↔ Array.reverse() (inverter), $arrayElemAt ↔ Array[index] (recuperar valor por índice), $size ↔ Array.length (comprimento do array). Essa correspondência permite que desenvolvedores front-end se familiarizem rapidamente com as operações de array do pipeline de agregação.
Armadilhas comuns em operações com matrizes: Existem várias armadilhas comuns em operações com matrizes — 1. O parâmetro cond de $filter deve retornar um valor booleano (expressões que retornam null ou undefined não filtrarão o elemento; em vez disso, ele será mantido); 2. O parâmetro initialValue de $reduce deve ser especificado (se não for especificado, o primeiro elemento da matriz é usado como valor inicial, mas se a matriz estiver vazia, null é retornado); 3. Índices negativos em $arrayElemAt são contados a partir do final (–1 é o último elemento, –2 é o penúltimo), mas se o índice estiver fora do intervalo, null é retornado; 4. $size gera um erro para os campos null ou undefined (requer proteção com $ifNull: ['$array', []]); 5. O parâmetro as de $map tem como valor padrão 'this', mas usar um nome personalizado melhora a legibilidade ($map: {input: '$tags', as: 'tag', in: {$toUpper: '$$tag'}}).
Escopo de $$this e $$value: $map/$filter/$reduce usam $$this para se referir ao elemento atual na matriz que está sendo iterada, enquanto $reduce usa $$value para se referir ao valor acumulado. Observe o sinal de dólar duplo — $$ denota variáveis do sistema ($$this, $$value, $$ROOT, $$DESCEND), enquanto um único $ denota uma referência a um campo. Erro comum: usar $field em vez de $$this.field na condição de $filter — $field se refere ao campo de nível superior do documento, enquanto $$this.field se refere ao campo do elemento atual da matriz. Compreender essas diferenças de escopo é fundamental para usar os operadores de matriz corretamente.
| Operador | Entrada | Saída | Variação no número de documentos |
|---|---|---|---|
| $map | N documentos | N documentos | Imutável (transformação interna de array) |
| $filter | N documentos | N documentos | Sem alterações (filtragem dentro da matriz) |
| $unwind | N documentos | N×M documentos | Expansão (divisão de submatrizes) |
Combinando $map e $filter: $map e $filter costumam ser usados juntos — primeiro, use $filter para selecionar os elementos desejados da matriz; depois, use $map para transformar o formato deles. Por exemplo: extrair apenas os nomes dos produtos dos itens enviados de um pedido — $map: {input: {$filter: {input: '$items', cond: {$eq: ['$$this.status', 'shipped']}}}, in: '$$this.name'}. Observe a ordem: aplique filter antes de map (para reduzir o número de elementos processados por map). A ordem inversa (aplicar map antes de filter) resulta em pior desempenho e menor legibilidade.
$reduce — Cálculo cumulativo: $reduce reduz um array a um único valor — sintaxe {input: array, initialValue: value, in: expression}. Cenários típicos: 1. Soma de um array $reduce: {initialValue: 0, in: {$add: ['$$value', '$$this']}}; 2. Concatenação de array $reduce: {valorInicial: '', in: {$concat: ['$$value', ',', '$$this']}}; 3. Valor máximo de um array $reduce: {valorInicial: 0, in: {$cond: [{$gt: ['$$this', '$$value']}, '$$this', '$$value']}}. $$value é o valor acumulado, $$this é o elemento atual.
graph LR
A["tags: ['5g','amoled','fast']"] --> B["$arrayElemAt: 0<br/>→ '5g'"]
A --> C["$size<br/>→ 3"]
A --> D["$map: {$toUpper}<br/>→ ['5G','AMOLED','FAST']"]
A --> E["$filter: len >= 3<br/>→ ['amoled','fast']"]
style B fill:#d4edda
style C fill:#cce5ff
style D fill:#fff3cd
style E fill:#e2d5f1
| Operador | Função | Equivalente em JavaScript |
|---|---|---|
$arrayElemAt |
Recuperar um elemento pelo índice | arr[index] |
$size |
Comprimento da matriz | arr.length |
$map |
Transformação elemento a elemento | arr.map(fn) |
$filter |
Filtro de condição | arr.filter(fn) |
$reduce |
Cálculo acumulado | arr.reduce(fn, init) |
$concatArrays |
Mesclar matrizes | [...a, ...b] |
$reverseArray |
Inverter um array | arr.reverse() |
Armadilhas comuns com $size: $size só pode ser usado com campos do tipo array — se o campo não for um array (por exemplo, se for nulo ou não existir), $size gerará um erro. Soluções alternativas: 1. Primeiro, use $ifNull: ['$tags', []] para converter o valor nulo em um array vazio; 2. Use $type: '$tags' para verificar se é um array antes de realizar o cálculo; 3. Defina default: [] no Esquema (para garantir que o campo seja sempre um array). $size retorna um inteiro e pode ser usado diretamente nas condições $cond — por exemplo, $cond: [{$gte: [{$size: '$tags'}, 3]}, 'Plenty of tags', 'Not enough tags'].
Índices negativos para $arrayElemAt: $arrayElemAt suporta índices negativos — -1 recupera o último elemento, e -2 recupera o penúltimo elemento, o que funciona da mesma forma que arr.at(-1) no JavaScript. Usos comuns: 1. Recuperar o registro mais recente {$arrayElemAt: ['$orders', -1]} (supondo que os pedidos estejam ordenados por data); 2. Recuperar a primeira tag: {$arrayElemAt: ['$tags', 0]}; 3. Recuperar o último endereço: {$arrayElemAt: ['$addresses', -1]}. Se o índice estiver fora do intervalo, $arrayElemAt retorna null (sem gerar um erro); você precisará usar $ifNull como alternativa.
Encadeamento de operações em matrizes: $map, $filter e $reduce podem ser encadeados — primeiro use $filter para filtrar, depois use $map para transformar e, por fim, use $reduce para agregar. Por exemplo: Cálculo do preço total dos itens enviados — $reduce: {input: {$map: {input: {$filter: {input: '$items', cond: {$eq: ['$$this.status', 'shipped']}}}, in: '$$this.price'}}, initialValue: 0, in: {$add: ['$$value', '$$this']}}. Observe a ordem de execução: primeiro o filtro (para reduzir o número de elementos processados pelo map), depois o map (para extrair o preço) e, por fim, o reduce (para somar os valores).
Otimização de desempenho para operações encadeadas: Embora as operações encadeadas em matrizes sejam poderosas, cada nível de aninhamento aumenta a carga computacional — 1. Otimize a ordem: use primeiro $filter (para reduzir o volume de dados para as operações subsequentes), depois $map (para extrair apenas os campos necessários) e, por fim, $reduce (para calcular o resultado final); 2. Evite cálculos redundantes: se várias operações encadeadas exigirem o mesmo resultado intermediário, use $addFields para calculá-lo uma vez e depois reutilizá-lo; 3. Observação sobre matrizes grandes: executar $map + $filter + $reduce em matrizes com mais de 1.000 elementos pode demorar muito; considere usar $unwind + $group como alternativa primeiro ($unwind processa matrizes grandes mais rapidamente na camada do banco de dados do que $map na camada de expressões); 4. Insira $match no início do pipeline: aplique operações em matrizes apenas aos documentos necessários (por exemplo, processe apenas a matriz items de pedidos pagos).
Três modos de acumulação do $reduce: O parâmetro in de $reduce determina o método de acumulação — 1. Acumulação numérica: in: {$add: ['$$value', '$$this']} (soma), in: {$max: ['$$value', '$$this']} (máximo); 2. Acumulação de strings: in: {$concat: ['$$value', ',', '$$this']} (concatenação separada por vírgulas); 3. Acumulação de objetos: in: {$mergeObjects: ['$$value', '$$this']} (mesclar propriedades de objetos). O parâmetro initialValue de $reduce determina o tipo do valor inicial e o tipo de saída final — um valor inicial numérico igual a 0 → gera um Number, um valor inicial de string igual a '' → gera um String e um valor inicial de objeto igual a {} → gera um Object. O tipo do valor inicial deve corresponder ao tipo de saída do parâmetro in.
// === $arrayElemAt Retrieve an Element by Index ===
db.products.aggregate([
{
$project: {
title: 1,
firstTag: { $arrayElemAt: ['$tags', 0] },
lastTag: { $arrayElemAt: ['$tags', -1] }
}
}
]);
// === $size Array Length ===
db.products.aggregate([
{
$project: {
title: 1,
tagCount: { $size: '$tags' }
}
}
]);
// === $map Array Transformations ===
db.products.aggregate([
{
$project: {
title: 1,
tagsUpper: {
$map: {
input: '$tags',
as: 'tag',
in: { $toUpper: '$$tag' }
}
}
}
}
]);
// === $filter Array Filtering ===
db.products.aggregate([
{
$project: {
title: 1,
expensiveTags: {
$filter: {
input: '$relatedProducts',
as: 'product',
cond: { $gte: ['$$product.price', 1000] }
}
}
}
}
]);
7. Treinamento prático abrangente
Lista de verificação para otimização de desempenho de pipelines de agregação: Os pipelines de agregação em ambientes de produção exigem otimização sistemática — 1. Coloque $match no início para filtrar os dados logo no início e reduzir o volume de dados; 2. Use $project após $group para simplificar os campos e reduzir o uso de memória; 3. Use $sort + $limit em vez da classificação completa (modo Top N); 4. Evite usar expressões complexas para o _id do $group (o que afeta a eficiência do agrupamento); 5. Defina allowDiskUse: true para grandes conjuntos de dados; 6. Use hint() para garantir o uso de índices e evitar varreduras completas da coleção; 7. Aplique $unwind imediatamente seguido de $group para evitar o inchaço de dados; 8. Os subpipelines de $facet compartilham entradas, mas operam de forma independente; controle o número de subpipelines.
Dicas para depuração de pipelines de agregação: Os pipelines de agregação utilizam chamadas encadeadas, o que torna os resultados intermediários invisíveis e dificulta a depuração. Três dicas práticas: 1. Execute etapa por etapa — adicione apenas uma etapa por vez e verifique se a saída corresponde às expectativas; 2. Use $project para reter apenas os campos-chave, reduzindo o ruído na saída para facilitar a análise; 3. Use o Construtor de Pipelines de Agregação do Compass para depuração visual. Se os resultados de $group não corresponderem às expectativas, verifique se o _id está correto — os erros mais comuns envolvem nomes de campos com erros ortográficos ou aspas ausentes no _id.
Aplicações comerciais da segmentação de usuários por RFM: O RFM (Recência/Frequência/Valor) é o modelo de segmentação de usuários mais utilizado no comércio eletrônico — 1. Recência (tempo desde a última compra): quanto menor o valor de daysSinceLastOrder, melhor (usuários que fizeram uma compra recentemente têm mais chances de comprar novamente); 2. Frequência (frequência de compra): quanto maior o valor de orderCount, melhor (usuários que compram com frequência são clientes fiéis); 3. Valor monetário (valor gasto): quanto maior o valor de totalSpent, melhor (usuários que gastam muito contribuem com a maior parte da receita). Cada uma das três dimensões do RFM é categorizada em níveis alto e baixo, resultando em 8 tipos de usuários — “Alto R, Alto F, Alto M” representa os usuários VIP mais valiosos, enquanto “Baixo R, Baixo F, Baixo M” representa usuários que abandonaram o serviço e precisam ser reativados ou baixados. O $switch mapeia as pontuações de RFM para tags de usuário e funciona como o canal de agregação para a análise de RFM.
Preparação e validação dos dados dos relatórios: Os dados de teste para os relatórios de vendas devem ser cuidadosamente elaborados — 1. Abranger um período de tempo suficientemente longo (pelo menos 3 meses; são necessários dados do mês anterior para comparações mês a mês); 2. Incluam valores atípicos (pedidos com valor de 0 yuan, pedidos reembolsados, para testar a robustez da filtragem $match); 3. Estejam razoavelmente distribuídos (muitos pedidos de baixo valor + poucos pedidos de alto valor, para simular a distribuição de pedidos reais); 4. Apresentem uma variedade de status (pagos/pendentes/reembolsados, para testar a eficácia da filtragem de $match: {status: 'paid'}). Após inserir os dados de teste em massa usando insertMany, verifique primeiro a correção dos dados com uma consulta simples find e, em seguida, execute o pipeline de agregação — problemas com os dados são mais comuns do que problemas com o pipeline.
O valor comercial dos relatórios de vendas: Os relatórios mensais de vendas são os dados essenciais para as operações de comércio eletrônico — eles respondem a três perguntas fundamentais: 1. Quais são as tendências (a receita está crescendo ou diminuindo)?; 2. Quais são as razões (qual categoria ou produto está impulsionando ou prejudicando o desempenho)?; 3. Como devemos nos ajustar (promoções, estoque ou estratégias de seleção de produtos)? Usar $setWindowFields + $shift para calcular o crescimento ano a ano é um passo crucial para elevar o relatório de simplesmente “exibir dados” para “fornecer insights” — saber que “a receita deste mês é de 3.000” tem menos valor do que saber que “a receita aumentou 100% em relação ao mês anterior”.
Comparação entre Aggregation Pipes e ferramentas de BI: Aggregation Pipes x ferramentas de BI, como Tableau e Metabase — os Aggregation Pipes são interfaces de programação (flexíveis, automatizadas e incorporáveis em aplicativos), enquanto as ferramentas de BI são interfaces visuais (arrastar e soltar, adequadas para usuários sem conhecimentos técnicos e com exploração interativa). Melhores práticas: 1. Use ferramentas de BI (conectadas por meio do MongoDB BI Connector) para consultas ad hoc realizadas pela equipe de operações; 2. Use o Aggregate Pipeline para relatórios fixos incorporados em aplicativos (desempenho controlável, resultados armazenáveis em cache); 3. Use Jupyter + PyMongo para análises aprofundadas realizadas por cientistas de dados (o ecossistema Python é mais robusto). O Aggregate Pipeline é adequado para necessidades analíticas “conhecidas e repetitivas”, enquanto as ferramentas de BI são adequadas para necessidades analíticas “desconhecidas e exploratórias”.
(1) Cenários empresariais complexos
Modelo de segmentação de usuários RFM: O RFM (Recency/Frequency/Monetary) é um modelo clássico para segmentar usuários de comércio eletrônico — “Recency” refere-se ao número de dias desde a última compra, “Frequency” refere-se à frequência de compra e “Monetary” refere-se ao valor acumulado dos gastos. Cada uma dessas três dimensões é categorizada em níveis alto, médio e baixo, resultando em 27 tipos distintos de usuários. Principais insights: R alto, F alta, M alta = Usuários VIP principais (requerem retenção prioritária); R baixa, F alta, M alta = Usuários em risco de cancelamento (requerem reengajamento); R alta, F baixa, M baixa = Novos usuários (requerem acompanhamento). Os pipelines de agregação do MongoDB são perfeitamente adequados para cálculos RFM — $group agrega as três dimensões por userId, e $switch mapeia os valores para os níveis alto, médio ou baixo.
Aplicações comerciais da segmentação de usuários: Os resultados da segmentação RFM orientam estratégias operacionais diferenciadas — 1. Usuários VIP: descontos exclusivos + entrega prioritária + atendimento ao cliente VIP; 2. Usuários em risco de cancelamento: cupons direcionados + mensagens de texto para reengajamento + ofertas especiais por tempo limitado; 3. Novos usuários: descontos na primeira compra + orientação para integração + recomendações de produtos; 4. Usuários inativos: reativação de baixo custo (notificações push em vez de mensagens de texto). Cada estratégia corresponde a diferentes custos de marketing — os usuários VIP apresentam o maior ROI (baixos custos de retenção), seguidos pelos novos usuários, enquanto os usuários inativos apresentam o menor.
// === Scene:User Segmentation Analysis ===
db.orders.aggregate([
{ $match: { status: 'paid' } },
{
$group: {
_id: '$userId',
totalSpent: { $sum: '$total' },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: '$total' },
firstOrderAt: { $min: '$createdAt' },
lastOrderAt: { $max: '$createdAt' }
}
},
{
$addFields: {
userLevel: {
$switch: {
branches: [
{ case: { $gte: ['$totalSpent', 10000] }, then: 'VIP' },
{ case: { $gte: ['$totalSpent', 1000] }, then: 'Gold' },
{ case: { $gte: ['$totalSpent', 100] }, then: 'Silver' }
],
default: 'Bronze'
}
},
daysSinceLastOrder: {
$divide: [
{ $subtract: [new Date(), '$lastOrderAt'] },
1000 * 60 * 60 * 24
]
}
}
},
{ $sort: { totalSpent: -1 } },
{ $limit: 100 }
]);
(2) Relatórios de vendas
Como funciona a função de janela $setWindowFields: $setWindowFields (MongoDB 5.0+) é uma função de janela no pipeline de agregação, semelhante à cláusula OVER() no SQL. Ela calcula valores agregados com base em janelas — como médias móveis, somas cumulativas e valores da linha anterior ou seguinte — para cada documento, sem alterar o número de documentos. $shift é a operação de deslocamento nas funções de janela; $shift: {output: '$revenue', by: -1} recupera o valor da receita da linha anterior para calcular a taxa de crescimento mês a mês.
Cálculo do crescimento mês a mês: Para calcular o crescimento ano a ano nos relatórios mensais, são necessários os dados do mês atual e do mês anterior — o SQL tradicional utiliza a função de janela LAG(), enquanto o MongoDB alcança a funcionalidade equivalente usando $setWindowFields + $shift. Fórmula de cálculo: (Mês atual - Mês anterior) / Mês anterior × 100%. Observe a proteção contra divisão por zero: quando o valor do mês anterior é 0, a taxa de crescimento é definida como 0 (verificada por meio de $cond).
Casos-limite para funções de janela: Quando $shift recupera a linha anterior para a primeira linha, ela retorna null (já que não há linha anterior) — é por isso que o cálculo de growthRate requer que $cond lide com casos em que prevMonthRevenue é null. Da mesma forma, recuperar a linha seguinte para a última linha também retorna null. Outras condições de limite para funções de janela: 1. Uma janela vazia retorna null; 2. Para uma janela de linha única, $sum é o valor dessa linha; 3. $rank e $denseRank se comportam de maneira diferente quando os valores estão empatados (o rank pula um número, enquanto o denseRank não). Compreender essas condições de limite é essencial para usar as funções de janela corretamente.
A arquitetura em camadas de um sistema de relatórios: Um sistema completo de relatórios consiste em três camadas — 1. Camada de dados (fluxo de agregação que calcula métricas brutas); 2. Camada de análise (métricas derivadas, como comparações mês a mês, ano a ano e classificações); 3. Camada de apresentação (formatação, gráficos e exportação). Neste exemplo, a camada de dados e a camada de análise são combinadas em um único pipeline de agregação, enquanto a camada de apresentação é gerenciada pelo front-end. Relatórios em um ambiente de produção normalmente também exigem: uma camada de cache (os dados dos relatórios são armazenados em cache a cada hora no Redis para evitar cálculos redundantes), uma camada de permissões (diferentes funções visualizam dados em diferentes dimensões) e uma camada de auditoria (que registra quem visualizou quais relatórios e quando).
// === Monthly Sales Report(Including year-over-year)===
db.orders.aggregate([
{
$group: {
_id: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' }
},
revenue: { $sum: '$total' },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: '$total' }
}
},
{ $sort: { '_id.year': 1, '_id.month': 1 } },
{
$setWindowFields: {
sortBy: { '_id.year': 1, '_id.month': 1 },
output: {
prevMonthRevenue: {
$shift: {
output: '$revenue',
by: -1
}
}
}
}
},
{
$addFields: {
growthRate: {
$cond: {
if: { $gt: ['$prevMonthRevenue', 0] },
then: {
$divide: [
{ $subtract: ['$revenue', '$prevMonthRevenue'] },
'$prevMonthRevenue'
]
},
else: 0
}
}
}
}
]);
Análise aprofundada do gerenciamento de memória do pipeline: Cada estágio de um pipeline de agregação mantém um fluxo de documentos na memória, com um limite padrão de 100 MB por estágio. Se esse limite for excedido, o MongoDB gera um erro e encerra o pipeline, a menos que allowDiskUse: true esteja definido (permitindo gravações temporárias no disco). No entanto, isso traz um novo problema: a E/S do disco é 100 vezes mais lenta que a memória, fazendo com que o desempenho do pipeline caia drasticamente. A abordagem correta é otimizar o pipeline para evitar o estouro: 1. Use $match no início para reduzir o volume de entrada; 2. Use $project para simplificar os campos e reduzir o espaço ocupado por documento; 3. Evite um número excessivo de valores únicos para _id em $group; 4. Esteja atento aos limites de tamanho de array em $push/$addToSet.
Lista de verificação prática para ajuste de desempenho de agregação: 1. Certifique-se de que o primeiro $match possa utilizar um índice (verifique a saída de explain); 2. A ordem ideal é $match antes de $group e $project após $group; 3. $sort + $limit podem ser otimizados para o modo Top N (requer apenas a manutenção de um heap de N elementos, em vez de uma ordenação completa); 4. Um $match que segue um $group pode ser movido para a frente (otimização manual — o MongoDB não faz isso automaticamente); 5. O foreignField em um $lookup deve ser indexado; 6. Para conjuntos de dados grandes, adicione maxTimeMS para evitar que o pipeline seja executado indefinidamente.
Casos-limite para a função de janela $setWindowFields: $setWindowFields é uma função de janela introduzida no MongoDB 5.0 que realiza cálculos (como médias móveis, somas cumulativas e classificações) em documentos dentro de uma “janela”. Casos-limite: 1. Limites da janela: “sem limite anterior” e “sem limite posterior” incluem todos os documentos do grupo, enquanto “1 anterior” e “1 posterior” incluem apenas documentos adjacentes; 2. Janela vazia: quando um grupo contém apenas um documento, $shift, $first e $last retornam null; 3. Conflitos de ordenação: o parâmetro sortBy deve corresponder à ordem de classificação do grupo; caso contrário, o intervalo da janela torna-se imprevisível; 4. Desempenho: as funções de janela devem manter o estado da janela na memória; grupos grandes (> 100.000 documentos) podem exceder os limites de memória.
Casos práticos de uso do $setWindowFields: O $setWindowFields preenche a lacuna deixada pela ausência de funções de janela no MongoDB — 1. Cálculos mês a mês/ano a ano: $shift recupera dados do período anterior, $subtract calcula a variação e $divide calcula a taxa de crescimento; 2. Somas cumulativas: a janela $sum abrange desde “sem limite anterior” até a linha atual para calcular as vendas acumuladas; 3. Médias móveis: a janela $avg utiliza dados dos N períodos mais recentes (por exemplo, os 7 períodos anteriores à linha atual) para suavizar flutuações de curto prazo; 4. Classificação/Paginação: $rank/$denseRank implementa a classificação, enquanto $rowNumber implementa a paginação (mais flexível do que skip/limit); 5. Top N agrupado: Após agrupar com partitionBy, use $rank para classificar os resultados e, em seguida, use $match com rank <= N para recuperar os N primeiros registros de cada grupo. Esses cenários representam usos padrão das funções de janela no SQL; a sintaxe do MongoDB é mais detalhada, mas funcionalmente equivalente.
A arquitetura em três camadas do sistema de relatórios: Um sistema de relatórios pronto para produção consiste em três camadas: 1. Camada de dados (pipeline de agregação do MongoDB): calcula resultados estatísticos a partir de dados brutos e os envia para uma coleção intermediária; 2. Camada de serviço (Node.js + cache): invoca o pipeline de agregação, armazena os resultados em cache (Redis com TTL de 5 minutos) e fornece APIs REST; 3. Camada de apresentação (biblioteca de gráficos front-end): recupera dados da API, renderiza gráficos (ECharts/Chart.js) e permite a filtragem interativa. Os benefícios de separar essas três camadas — a camada de dados se concentra no cálculo, a camada de serviço se concentra no desempenho e a camada de apresentação se concentra na experiência do usuário — permitem que cada camada seja otimizada e dimensionada de forma independente.
Pipelines de agregação x Ferramentas de BI: Quando se deve usar pipelines de agregação para criar relatórios e quando se deve recorrer a ferramentas especializadas de BI (Metabase/Superset/Tableau)? Os pipelines de agregação são adequados para: 1. Relatórios com lógica simples (pipelines de agregação com 5 a 10 etapas); 2. Aplicativos que exigem incorporação (a API retorna os dados do relatório, e o front-end gera os gráficos por conta própria); 3. Volume moderado de dados (< 10 milhões de registros); 4. Altos requisitos de tempo real (cálculo em tempo real para cada solicitação). As ferramentas de BI são adequadas para: 1. Consultas de autoatendimento por usuários sem conhecimentos técnicos (interface do tipo arrastar e soltar); 2. Análises multidimensionais complexas (cubos OLAP, drill-down/drill-up); 3. Fontes de dados diversas (MongoDB + MySQL + CSV); 4. Envio programado de relatórios por e-mail. Para equipes pequenas, um pipeline de agregação combinado com o ECharts é suficiente; para equipes maiores, as ferramentas de BI oferecem maior eficiência.
▶ Exemplo 1: Aplicação prática avançada de pipelines de agregação — Análise de segmentação de usuários
// Scene:Classify users based on their spending VIP Layering
db.orders.insertMany([
{ userId: 'user_001', total: NumberDecimal('15000'), createdAt: new Date('2026-06-01'), status: 'paid' },
{ userId: 'user_002', total: NumberDecimal('500'), createdAt: new Date('2026-06-05'), status: 'paid' },
{ userId: 'user_003', total: NumberDecimal('50'), createdAt: new Date('2026-06-10'), status: 'paid' },
{ userId: 'user_001', total: NumberDecimal('800'), createdAt: new Date('2026-06-15'), status: 'paid' }
]);
// Complete Pipeline:User Segmentation + Tag Conversion + Monthly Statistics
db.orders.aggregate([
// Step 1: Count only paid orders
{ $match: { status: 'paid' } },
// Step 2: Group by User
{
$group: {
_id: '$userId',
totalSpent: { $sum: '$total' },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: '$total' },
lastOrderAt: { $max: '$createdAt' }
}
},
// Step 3: Use $switch to Segment Users
{
$addFields: {
userLevel: {
$switch: {
branches: [
{ case: { $gte: ['$totalSpent', 10000] }, then: 'VIP' },
{ case: { $gte: ['$totalSpent', 1000] }, then: 'Gold' },
{ case: { $gte: ['$totalSpent', 100] }, then: 'Silver' }
],
default: 'Bronze'
}
},
// Days until the last order
daysSinceLastOrder: {
$divide: [
{ $subtract: [new Date(), '$lastOrderAt'] },
1000 * 60 * 60 * 24
]
}
}
},
// Step 4: Date Format
{
$project: {
userId: '$_id',
totalSpent: 1,
avgOrderValue: { $toString: '$avgOrderValue' }, // Decimal128 -> String
userLevel: 1,
daysSinceLastOrder: { $round: ['$daysSinceLastOrder', 0] }, // Round to the nearest whole number
lastOrderDate: {
$dateToString: {
format: '%Y-%m-%d',
date: '$lastOrderAt',
timezone: 'Asia/Tokyo'
}
}
}
},
// Step 5: Sort by Amount Spent
{ $sort: { totalSpent: -1 } }
]);
// Output Results:
// [
// { userId: 'user_001', totalSpent: '15800', avgOrderValue: '7900', userLevel: 'VIP', daysSinceLastOrder: 16, lastOrderDate: '2026-06-15' },
// { userId: 'user_002', totalSpent: '500', avgOrderValue: '500', userLevel: 'Silver', daysSinceLastOrder: 26, lastOrderDate: '2026-06-05' },
// { userId: 'user_003', totalSpent: '50', avgOrderValue: '50', userLevel: 'Bronze', daysSinceLastOrder: 21, lastOrderDate: '2026-06-10' }
// ]
Resultado: Os três usuários são segmentados automaticamente de acordo com o valor gasto. O usuário user_001 tem um gasto total de 15.800 e está marcado como VIP; já se passaram 16 dias desde seu último pedido, e a data está formatada para o fuso horário de Tóquio.
▶ Exemplo: Monthly Sales Report com Month-over-Month Growth (Difficulty ⭐⭐)
// Scene: ShopHub sales dashboard comparing current month vs previous month
db.orders.insertMany([
{ orderId: 'ORD-001', total: 15000, status: 'paid', createdAt: new Date('2026-06-15') },
{ orderId: 'ORD-002', total: 8500, status: 'paid', createdAt: new Date('2026-06-25') },
{ orderId: 'ORD-003', total: 12000, status: 'paid', createdAt: new Date('2026-07-05') },
{ orderId: 'ORD-004', total: 9500, status: 'paid', createdAt: new Date('2026-07-15') },
{ orderId: 'ORD-005', total: 18000, status: 'paid', createdAt: new Date('2026-07-20') }
]);
// Pipeline: monthly aggregation + window function for growth rate
const monthlyReport = db.orders.aggregate([
// Stage 1: Match paid orders only
{ $match: { status: 'paid' } },
// Stage 2: Group by year-month
{ $group: {
_id: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' }
},
revenue: { $sum: '$total' },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: '$total' }
}},
// Stage 3: Sort chronologically
{ $sort: { '_id.year': 1, '_id.month': 1 } },
// Stage 4: Use window function to get previous month revenue
{ $setWindowFields: {
sortBy: { '_id.year': 1, '_id.month': 1 },
output: {
prevRevenue: { $shift: { output: '$revenue', by: -1 } }
}
}},
// Stage 5: Calculate growth rate
{ $addFields: {
growthRate: {
$cond: {
if: { $gt: ['$prevRevenue', 0] },
then: {
$multiply: [
{ $divide: [{ $subtract: ['$revenue', '$prevRevenue'] }, '$prevRevenue'] },
100
]
},
else: 0
}
}
}},
// Stage 6: Format output
{ $project: {
month: { $concat: [
{ $toString: '$_id.year' }, '-',
{ $toString: '$_id.month' }
]},
revenue: 1,
orderCount: 1,
avgOrderValue: { $round: ['$avgOrderValue', 2] },
growthRate: { $round: ['$growthRate', 1] }
}}
]);
monthlyReport.forEach(r => console.log(JSON.stringify(r)));
Saída:
TEXT 📖 Somente leitura{"month":"2026-6","revenue":23500,"orderCount":2,"avgOrderValue":11750,"growthRate":0} {"month":"2026-7","revenue":39500,"orderCount":3,"avgOrderValue":13166.67,"growthRate":68.1}
▶ Exemplo 2: Relatórios de vendas do ShopHub + Formatação de datas
Preparação de dados para relatórios: Os dados de teste para relatórios de vendas devem abranger vários meses — caso contrário, não será possível calcular o crescimento mês a mês (já que o primeiro mês não possui dados do mês anterior). Neste exemplo, preparamos dados de pedidos de maio a julho: 1 pedido em maio, 2 pedidos em junho e 1 pedido em julho. Pontos-chave para o projeto dos dados: 1. Pelo menos 1 pedido por mês (caso contrário, o $bucket gerará buckets vazios); 2. A distribuição dos valores é razoável (incluindo um valor pequeno de 300 e um valor grande de 2.200); 3. O status está uniformemente definido como paid ($match filtra os pedidos não pagos).
Interpretando o crescimento mês a mês: Taxa de crescimento mês a mês = (Este mês - Mês anterior) / Mês anterior. A receita de 3.000 em junho representa um aumento de 100% em relação aos 1.500 de maio — trata-se de uma “duplicação” do crescimento. No entanto, é preciso estar atento ao efeito de base — um aumento de 100 para 200 também representa um crescimento de 100%, mas o aumento absoluto é de apenas 100; um aumento de 10.000 para 15.000 representa apenas 50% de crescimento, mas o aumento absoluto é de 5.000. Ao tomar decisões de negócios, é essencial considerar tanto a taxa de crescimento quanto o aumento absoluto; basear-se em apenas uma métrica é insuficiente.
Métodos de validação de dados para relatórios: A saída do pipeline de agregação deve ser validada — 1. Validação cruzada: compare o resultado de find().count() com o resultado de $sum: 1 em $group; as contagens devem coincidir; 2. Validação por amostragem: selecione aleatoriamente 2 a 3 linhas de dados brutos e calcule-as manualmente para verificar se os resultados agregados estão corretos; 3. Validação de limites: Para um conjunto de dados vazio ($match não retorna nenhuma correspondência) → Resultado vazio em vez de um erro; linha única de dados ($group contém apenas 1 grupo) → Taxa de crescimento mês a mês é 0 (sem dados do mês anterior); 4. Validação de consistência: Total mensal = Total anual; totais de cada categoria = Total geral. Qualquer inconsistência indica um bug na lógica do pipeline.
Estratégia de armazenamento em cache de relatórios: Os pipelines de agregação em tempo real levam muito tempo para processar grandes conjuntos de dados (segundos) — 1. Materialização programada: Use $merge para gravar os resultados da agregação em uma coleção de relatórios (por exemplo, monthly_reports) a cada hora; as consultas são feitas a partir da coleção de relatórios (milissegundos); 2. Atualizações incrementais: agregue apenas dados novos ($match com um intervalo de tempo incremental) e, em seguida, use $merge para mesclá-los aos relatórios existentes; 3. Camada de cache: use Node.js para armazenar em cache os resultados da agregação no Redis (TTL de 5 a 30 minutos), adequado para páginas de relatórios com muitas leituras e poucas gravações; 4. Estratégia de expiração: marque o cache como inválido quando os dados de origem forem alterados (usando números de versão ou carimbos de data/hora) e recalcule na próxima consulta. Critérios para selecionar uma estratégia: frequência de alteração dos dados (requisitos em tempo real) × frequência de consultas (requisitos de desempenho).
// Scene:ShopHub The operations team generates monthly sales reports.,Includes formatted dates and year-over-year growth
db.orders.insertMany([
{ orderId: 'ORD-001', userId: 'user_001', total: NumberDecimal('1500'), status: 'paid', createdAt: new Date('2026-05-15') },
{ orderId: 'ORD-002', userId: 'user_002', total: NumberDecimal('800'), status: 'paid', createdAt: new Date('2026-06-01') },
{ orderId: 'ORD-003', userId: 'user_001', total: NumberDecimal('2200'), status: 'paid', createdAt: new Date('2026-06-20') },
{ orderId: 'ORD-004', userId: 'user_003', total: NumberDecimal('300'), status: 'paid', createdAt: new Date('2026-07-05') }
]);
// Monthly Report:Format Month、Calculate the month-over-month growth、User Segmentation Labels
db.orders.aggregate([
{ $match: { status: 'paid' } },
{
$group: {
_id: {
year: { $year: '$createdAt' },
month: { $month: '$createdAt' }
},
revenue: { $sum: '$total' },
orderCount: { $sum: 1 },
avgOrderValue: { $avg: '$total' }
}
},
{ $sort: { '_id.year': 1, '_id.month': 1 } },
{
$addFields: {
monthLabel: {
$dateToString: {
format: '%Y-%m',
date: { $dateFromParts: { year: '$_id.year', month: '$_id.month' } }
}
},
revenueStr: { $toString: '$revenue' },
performance: {
$switch: {
branches: [
{ case: { $gte: ['$revenue', 2000] }, then: 'Excellent' },
{ case: { $gte: ['$revenue', 1000] }, then: 'Good' },
{ case: { $gte: ['$revenue', 500] }, then: 'Average' }
],
default: 'Below Target'
}
}
}
}
]);
// Output:
// [
// { _id: {year:2026,month:5}, monthLabel:'2026-05', revenue:1500, performance:'Good', ... },
// { _id: {year:2026,month:6}, monthLabel:'2026-06', revenue:3000, performance:'Excellent', ... },
// { _id: {year:2026,month:7}, monthLabel:'2026-07', revenue:300, performance:'Below Target', ... }
// ]
Saída: Os relatórios mensais incluem rótulos de mês formatados (2026-05), valores de receita convertidos e tags automáticas de classificação de desempenho usando $switch.
Adaptação do sistema de relatórios para produção: O pipeline de agregação neste exemplo é uma versão de treinamento — são necessárias modificações adicionais para o ambiente de produção — 1. Consultas parametrizadas: Intervalos de meses, filtros de categoria e filtros de ID de usuário devem ser passados como parâmetros de API (em vez de serem codificados diretamente no pipeline); 2. Tratamento de erros: a execução do pipeline pode falhar devido a limites de memória ou tempos limite; isso requer envolver o código em um bloco try-catch, definir um limite maxTimeMS e usar allowDiskUse como alternativa; 3. Camada de cache: os dados dos relatórios mensais mudam com pouca frequência (apenas alguns novos pedidos por dia); use o Redis para armazenar em cache os resultados da agregação (TTL de 1 hora), de modo que 90% das solicitações de relatório sejam atendidas pelo cache e não exijam a execução do pipeline; 4. Materialização programada: use o Change Stream para monitorar alterações nos pedidos e atualizar a coleção de relatórios de forma incremental (em vez de realizar uma agregação completa a cada vez); 5. Adaptação do formato de saída: o front-end exige o formato Chart.js ({labels: [...], datasets: [...]}), portanto, os resultados agregados são convertidos para o formato de gráfico no back-end.
Lista de verificação para solução de problemas no pipeline de agregação: Etapas para solucionar erros no pipeline de agregação — 1. Classificação do tipo de erro: “Buffer excede o limite” → Limite de memória excedido (aumente allowDiskUse ou otimize o pipeline); “limite de tempo excedido” → Tempo limite de execução excedido (aumente maxTimeMS ou otimize os índices); “o caminho do campo deve começar com '$'” → Erro de referência de campo (verifique se há o prefixo $); “operador desconhecido” → Erro ortográfico no operador ou operador não suportado pela versão; 2. Depure etapa por etapa: execute apenas uma etapa por vez; após confirmar que a saída está correta, prossiga para a próxima etapa; 3. Verificação do volume de dados: verifique se o número de documentos antes e depois de $group corresponde às expectativas (quantos foram filtrados por $match e quantos foram expandidos por $unwind); 4. Verificação do índice: use explain() para confirmar que $match está usando um índice (IXSCAN em vez de COLLSCAN); 5. Compatibilidade de versão: Operadores como $dateAdd (5.0+), $setWindowFields (5.0+) e $densify (6.1+) exigem a versão correspondente do MongoDB.
❓ Perguntas Frequentes
A intenção por trás das perguntas comuns: Essas perguntas não são apenas perguntas frequentes; elas são uma extensão das decisões de projeto — a escolha entre $cond e $switch envolve um equilíbrio entre legibilidade e desempenho; os fusos horários envolvem a escolha de uma estratégia de armazenamento; e a conversão de tipos envolve a gestão de riscos em um sistema de tipagem fraca. Entender o “porquê” é mais importante do que lembrar o “o quê”.
P: Qual tem melhor desempenho, $cond ou $switch? R: $cond é um pouco mais rápido (menos instruções de CPU). Use $switch apenas quando houver múltiplas ramificações.
P: O $dateToString suporta fusos horários? R: Ele suporta o parâmetro
timezone(nomes de fusos horários da IANA, como 'Asia/Tokyo').
P: O que acontece se uma conversão de tipo falhar? R: Por padrão, ela retorna null. Você pode usar
$convertpara especificar um manipulador onError.
📖 Resumo
- Expressões condicionais: $cond (if-else), $switch (ramificações múltiplas), $ifNull (tratamento de valores nulos)
- Operações com datas: $year/$month/$dayOfWeek + $dateToString
- Conversão de tipos: $toString/$toInt/$toDecimal/$toDate
- Operações com strings: $concat, $substr, $toUpper
- Operações com matrizes: $arrayElemAt/$size/$map/$filter
Conectando conceitos: Os cinco tópicos avançados no pipeline de agregação formam a pilha de recursos de processamento de dados — as expressões condicionais constituem a “camada lógica” (tomada de decisões com base nos dados), enquanto as operações com data, tipo, string e matriz constituem a “camada de transformação” (conversão dos dados para o formato desejado). Em pipelines reais, essas operações são usadas em combinação: $group calcula estatísticas → $addFields aplica camadas condicionais com $switch → $project formata datas com $dateToString → saída. Dominar a camada de transformação é o caminho essencial para se tornar proficiente em pipelines de agregação.
Uma visão geral abrangente da otimização do desempenho do pipeline de agregação: O desempenho de um pipeline de agregação depende de três fatores — 1. Volume de E/S (número de documentos analisados/entradas de índice lidas): isso pode ser otimizado adicionando-se um pré-filtro antes do estágio $match e utilizando hint() para selecionar índices; 2. Uso de memória (quanta memória os resultados intermediários do pipeline consomem): a otimização pode ser alcançada simplificando os campos em $project e usando allowDiskUse para descarregar o excesso para o disco; 3. Computação da CPU (complexidade computacional de $group/$sort): a otimização pode ser alcançada reduzindo a complexidade de _id e aproveitando os índices para a classificação. Cada dimensão possui princípios de projeto e técnicas de ajuste correspondentes; uma compreensão sistemática é mais eficaz do que memorizar truques de otimização um por um.
De pipelines de agregação ao ETL: Um pipeline de agregação é, essencialmente, uma ferramenta ETL (Extract-Transform-Load) leve — $match corresponde à etapa de Extração (extração de dados de uma coleção), $project/$addFields/$convert corresponde à etapa de Transformação (limpeza e transformação de dados) e $out/$merge corresponde à etapa de Carregamento (gravação na coleção de destino). Para pipelines de dados simples (fonte única → transformação → destino), o pipeline de agregação é mais leve e rápido do que o Spark ou o Airflow. No entanto, quando o ETL envolve múltiplas fontes de dados (MongoDB + MySQL + S3) ou agendamento complexo (cadeias de dependências, novas tentativas, alertas), você deve usar uma ferramenta ETL dedicada em vez do pipeline de agregação.
📝 Exercícios
Nível de dificuldade: Os 5 exercícios correspondem a 3 níveis de dificuldade — os Exercícios Básicos (⭐) testam sua capacidade de usar um único operador; os Exercícios Intermediários (⭐⭐) testam sua capacidade de combinar operadores; e os Exercícios de Desafio (⭐⭐⭐) testam sua capacidade de projetar de forma independente. Recomendamos que você os realize em ordem. Para cada exercício, primeiro descreva em chinês o que cada etapa do pipeline deve fazer (pseudo-pipeline) e, em seguida, traduza isso em código.
- Questão básica (⭐): Use uma instrução $switch para adicionar nomes em chinês aos status dos pedidos.
- Questão básica (⭐): Use $dateToString para formatar a data do pedido.
- Problema avançado (⭐⭐): Segmentação de usuários ($switch para distinguir entre VIP, Ouro e Prata).
- Problema avançado (⭐⭐): Use $map para converter todos os rótulos para maiúsculas.
- Questão desafiadora (⭐⭐⭐): Relatório mensal de vendas + taxa de crescimento ano a ano ($setWindowFields + $shift).
Guia do Desafio: O desafio “Relatório Mensal de Vendas + Taxa de Crescimento Anual” é o que mais se assemelha a cenários empresariais do mundo real. Etapas de implementação: 1. Use $match para filtrar os pedidos pagos; 2. Use $group para agrupar por {ano, mês} e calcular a receita, o número de pedidos e o valor médio por pedido; 3. Use $sort para ordenar por ano e mês; 4. Use $setWindowFields e $shift para recuperar a receita do mês anterior; 5. Use $addFields para calcular a taxa de crescimento mês a mês. Observe que, em $shift, by: -1 se refere à “linha anterior” (mês anterior) e by: 1 se refere à “linha seguinte” (mês seguinte).