Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

Aggregation pipeline no MongoDB: relatório passo a passo

Monte pipelines com match, unwind, group, project, sort e lookup para gerar relatórios de vendas no MongoDB, com saídas e erro reais.

Rodolfo Mori5 min de leitura

Aggregation pipeline é uma sequência de estágios que filtra, abre, agrupa, calcula e reorganiza documentos. Nesta lição, você vai transformar pedidos da Livraria Horizonte em um ranking de faturamento por livro e num resumo por cliente.

O método técnico é collection.aggregate(). Ele recebe um array de estágios; cada estágio lê a saída do anterior. O resultado é um cursor, consumido aqui com toArray() porque o conjunto final é pequeno.

A bancada em que cada pessoa faz uma etapa

Imagine a conferência de vendas numa bancada. A primeira pessoa separa pedidos pagos. A segunda abre a lista de itens e coloca cada item numa ficha própria. A terceira reúne fichas do mesmo livro. A quarta soma unidades e valor. A última ordena o ranking.

Esse mapa corresponde a $match, $unwind, $group, $project e $sort. Quando um estágio entrega seis fichas, o próximo trabalha sobre essas seis, não sobre os pedidos originais.

O limite da analogia: MongoDB não precisa materializar uma pilha física entre todos os estágios e pode otimizar a execução. Alguns estágios usam índices, memória ou disco; outros mudam a quantidade e o formato dos documentos. A ordem continua sendo parte do resultado e do custo, então não reorganize a pipeline apenas porque os nomes “parecem equivalentes”.

Para revisar filtros e cursores antes de agregar, passe pelo CRUD MongoDB com Node.js.

Monte o laboratório e conheça a entrada

Com MongoDB 8.0.28 ativo, instale o driver oficial:

bash
npm init -y
npm install mongodb@7.5.0
added 12 packages

Crie aggregation.mjs, conecte e use um database descartável:

js
import { MongoClient } from "mongodb";

const uri = process.env.MONGODB_URI ?? "mongodb://127.0.0.1:27017";
const client = new MongoClient(uri);
await client.connect();

const db = client.db("devclub_aggregation");
await db.dropDatabase();
const pedidos = db.collection("pedidos");
const clientes = db.collection("clientes");

console.log({ database: db.databaseName, estado: "limpo" });
{ database: 'devclub_aggregation', estado: 'limpo' }

Cadastre dois clientes e quatro pedidos. Três estão pagos; o cancelado contém cinco unidades de Node.js justamente para comprovar que o filtro acontece antes do ranking:

js
await clientes.insertMany([
  { _id: "cliente-1", nome: "Ana" },
  { _id: "cliente-2", nome: "Bruno" },
]);

await pedidos.insertMany([
  {
    _id: "p1", clienteId: "cliente-1", status: "pago", total: 134.3,
    itens: [
      { titulo: "JavaScript do zero", quantidade: 2, precoUnitario: 39.9 },
      { titulo: "Node.js na prática", quantidade: 1, precoUnitario: 54.5 },
    ],
  },
  {
    _id: "p2", clienteId: "cliente-2", status: "pago", total: 99.7,
    itens: [
      { titulo: "JavaScript do zero", quantidade: 1, precoUnitario: 39.9 },
      { titulo: "CSS sem sustos", quantidade: 2, precoUnitario: 29.9 },
    ],
  },
  {
    _id: "p3", clienteId: "cliente-2", status: "cancelado", total: 272.5,
    itens: [
      { titulo: "Node.js na prática", quantidade: 5, precoUnitario: 54.5 },
    ],
  },
  {
    _id: "p4", clienteId: "cliente-1", status: "pago", total: 148.9,
    itens: [
      { titulo: "Node.js na prática", quantidade: 2, precoUnitario: 54.5 },
      { titulo: "JavaScript do zero", quantidade: 1, precoUnitario: 39.9 },
    ],
  },
]);

console.log({ clientes: 2, pedidos: 4 });
{ clientes: 2, pedidos: 4 }

Os totais foram escritos com Number para manter o foco no pipeline. Para dinheiro em produção, defina centavos inteiros ou Decimal128 e uma regra de arredondamento. Um relatório correto sobre um tipo inadequado continua sendo um relatório inadequado.

Primeiro estágio: $match reduz o conjunto

Comece respondendo apenas “quantos pedidos pagos seguem adiante?”. $count é usado temporariamente para enxergar a saída do filtro:

js
const depoisDoMatch = await pedidos.aggregate([
  { $match: { status: "pago" } },
  { $count: "pedidosPagos" },
]).toArray();

console.log(depoisDoMatch);
[ { pedidosPagos: 3 } ]

Colocar $match cedo evita que o pedido cancelado atravesse as contas. Quando o filtro corresponde a um índice e aparece numa posição aproveitável, ele também pode reduzir leitura. A lição de índices MongoDB mede essa diferença com explain().

$unwind transforma cada item em uma entrada

Cada pedido tem um array itens. Para agrupar por título, precisamos de um documento de pipeline por item. $unwind abre o array e repete os outros campos do pedido em cada saída:

js
const depoisDoUnwind = await pedidos.aggregate([
  { $match: { status: "pago" } },
  { $unwind: "$itens" },
  { $count: "itensNosPedidosPagos" },
]).toArray();

console.log(depoisDoUnwind);
[ { itensNosPedidosPagos: 6 } ]

Há três pedidos pagos e dois tipos de item em cada um, portanto saem seis entradas. quantidade: 2 não cria duas entradas: é um valor do item, somado no estágio seguinte. Confundir “linhas do array” com “unidades vendidas” produz um ranking errado sem gerar exceção.

$group cria os baldes e faz as contas

_id dentro de $group é a chave de agrupamento, não o _id do pedido. Todos os itens com o mesmo título entram no mesmo grupo. $sum acumula quantidades; $multiply calcula o valor de cada item antes da soma:

js
const grupos = await pedidos.aggregate([
  { $match: { status: "pago" } },
  { $unwind: "$itens" },
  {
    $group: {
      _id: "$itens.titulo",
      unidades: { $sum: "$itens.quantidade" },
      faturamento: {
        $sum: {
          $multiply: ["$itens.quantidade", "$itens.precoUnitario"],
        },
      },
    },
  },
  { $sort: { _id: 1 } },
]).toArray();

console.log(grupos);
[ { _id: 'CSS sem sustos', unidades: 2, faturamento: 59.8 }, { _id: 'JavaScript do zero', unidades: 4, faturamento: 159.6 }, { _id: 'Node.js na prática', unidades: 3, faturamento: 163.5 } ]

O pedido cancelado tinha cinco unidades de Node.js, mas o grupo mostra três. Essa diferença é a evidência de que $match protegeu o cálculo.

$project entrega o formato da resposta

O consumidor do relatório não precisa saber que $group chamou o título de _id. $project renomeia o campo, omite o id interno e arredonda o total; o $sort final ordena pelo faturamento:

js
const ranking = await pedidos.aggregate([
  { $match: { status: "pago" } },
  { $unwind: "$itens" },
  {
    $group: {
      _id: "$itens.titulo",
      unidades: { $sum: "$itens.quantidade" },
      faturamento: {
        $sum: { $multiply: ["$itens.quantidade", "$itens.precoUnitario"] },
      },
    },
  },
  {
    $project: {
      _id: 0,
      livro: "$_id",
      unidades: 1,
      faturamento: { $round: ["$faturamento", 2] },
    },
  },
  { $sort: { faturamento: -1 } },
]).toArray();

console.log(ranking);
[ { unidades: 3, livro: 'Node.js na prática', faturamento: 163.5 }, { unidades: 4, livro: 'JavaScript do zero', faturamento: 159.6 }, { unidades: 2, livro: 'CSS sem sustos', faturamento: 59.8 } ]

O formato final é uma decisão de contrato. Uma pipeline não precisa devolver documentos com a mesma forma da coleção de origem. Isso a torna útil para dashboards e respostas de API, mas também exige testes: mudar um estágio pode alterar nomes ou tipos consumidos pela interface.

$lookup liga o agrupamento ao cadastro atual

Para o resumo por cliente, agrupe primeiro por clienteId; depois busque o nome na coleção clientes. $lookup devolve um array chamado cliente, mesmo quando há uma única combinação:

js
const porCliente = await pedidos.aggregate([
  { $match: { status: "pago" } },
  {
    $group: {
      _id: "$clienteId",
      pedidos: { $sum: 1 },
      gasto: { $sum: "$total" },
    },
  },
  {
    $lookup: {
      from: "clientes",
      localField: "_id",
      foreignField: "_id",
      as: "cliente",
    },
  },
  {
    $project: {
      _id: 0,
      cliente: { $first: "$cliente.nome" },
      pedidos: 1,
      gasto: { $round: ["$gasto", 2] },
    },
  },
  { $sort: { gasto: -1 } },
]).toArray();

console.log(porCliente);
[ { pedidos: 2, cliente: 'Ana', gasto: 283.2 }, { pedidos: 1, cliente: 'Bruno', gasto: 99.7 } ]

Se um clienteId não encontrar cadastro, $first devolve ausência de valor. Defina o que o relatório faz com referência órfã; esconder silenciosamente pode mascarar defeito de dados. A decisão entre combinar e incorporar começa na modelagem MongoDB, não no estágio $lookup isolado.

Um erro de expressão também interrompe a pipeline

MongoDB valida e executa expressões no servidor. Dividir cada total por zero faz a operação falhar com MongoServerError e código 2:

js
try {
  await pedidos.aggregate([
    { $project: { taxa: { $divide: ["$total", 0] } } },
  ]).toArray();
} catch (erro) {
  console.log({
    erro: "divisão por zero no pipeline",
    codigo: erro.code,
    nome: erro.name,
  });
}
{ erro: 'divisão por zero no pipeline', codigo: 2, nome: 'MongoServerError' }

Num cálculo real, trate denominador zero com regra explícita, por exemplo $cond, quando zero tiver significado válido. Capturar a exceção e devolver zero para qualquer falha esconderia problemas diferentes sob o mesmo valor.

Missão: calcule o ticket médio sem dividir no escuro

Amplie o pipeline por cliente com ticketMedio, dividindo gasto por pedidos. Como cada grupo tem ao menos um pedido, o denominador é conhecido:

js
const ticket = await pedidos.aggregate([
  { $match: { status: "pago" } },
  {
    $group: {
      _id: "$clienteId",
      quantidade: { $sum: 1 },
      gasto: { $sum: "$total" },
    },
  },
  {
    $project: {
      _id: 0,
      clienteId: "$_id",
      ticketMedio: { $round: [{ $divide: ["$gasto", "$quantidade"] }, 2] },
    },
  },
  { $sort: { clienteId: 1 } },
]).toArray();

console.log(ticket);
await client.close();
[ { clienteId: 'cliente-1', ticketMedio: 141.6 }, { clienteId: 'cliente-2', ticketMedio: 99.7 } ]

A missão termina quando Ana aparece com 141.6, resultado de 283.2 dividido por dois pedidos, e Bruno com 99.7. Depois remova $match de propósito: o valor de Bruno muda porque a venda cancelada entra. Essa comparação mostra que um relatório pode rodar sem erro e ainda responder à pergunta errada.

  • mongodb
  • aggregation pipeline
  • group
  • lookup
  • relatorios

Perguntas frequentes

O que é aggregation pipeline no MongoDB?
É uma sequência de estágios que recebe documentos, transforma ou filtra cada conjunto e passa o resultado ao estágio seguinte. Ela serve para agrupamentos, cálculos, reformatação e combinação de coleções.
aggregate altera os documentos originais?
Não nas pipelines mostradas aqui. Uma pipeline de leitura só produz resultados. Estágios específicos, como out e merge, podem gravar dados e precisam de cuidado separado.
Por que colocar match no começo?
Filtrar cedo reduz os documentos que atravessam os estágios seguintes e pode permitir uso de índice. Nem toda pipeline aceita qualquer ordem, por isso confirme o plano e o resultado.
lookup é igual a JOIN em SQL?
Ele cumpre uma função parecida ao combinar documentos por campos, mas a saída é um array e as opções e custos seguem o modelo do MongoDB. A modelagem deve decidir se a combinação frequente merece incorporação.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em MongoDB 8.0.28, MongoDB Node.js Driver 7.5.0, Node 26.3.0 e Docker 29.5.3, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MongoDB Manual — Aggregation Operations — mongodb.com
  2. MongoDB Manual — Aggregation Pipeline — mongodb.com
  3. MongoDB Manual — Aggregation Stages — mongodb.com
  4. MongoDB Node.js Driver — Aggregation — mongodb.com

Continue por aqui