Como funcionam joins em bancos de documentos?

Atualizado em: agosto de 2026

Uma consulta relacional em banco de documentos é um join feito com referências e lookups em vez de chaves estrangeiras — ou evitado ao embutir os dados. Esse “ou” é o assunto inteiro: bancos de documentos dão três formas de relacionar dados, e a decisão de design é quando o join acontece — na escrita (embutir), na consulta dentro do servidor (lookup), ou na consulta dentro da aplicação (referências mais uma busca de acompanhamento).

Principais pontos

PerguntaResposta
Bancos de documentos fazem join?Sim — lookups fazem left outer joins, retornando arrays, não linhas
A decisão realEmbutir vs. referenciar — quando o join acontece?
Regra padrãoEmbuta o que é lido junto e limitado; referencie o que vive só ou cresce
A verdade de performanceLookups no servidor são joins de loop aninhado — bons por requisição, errados para analytics
O bug clássicoQueries N+1 — resolvidas com batch, include ou embutindo

As três formas de relacionar documentos

// 1 · Embutir — o "join" aconteceu na hora da escrita
{ _id: 1, title: "Dune", author: { name: "Frank Herbert", born: 1920 } }

// 2 · Referência + $lookup — o join acontece na consulta, no servidor
db.books.aggregate([
  { $lookup: { from: "authors", localField: "authorId",
               foreignField: "_id", as: "author" } },  // left outer join → array
  { $unwind: "$author" }                               // achata → inner join
])

// 3 · Referência + join no lado da aplicação — o caminho de ODM/SDK (abaixo)

O terceiro caminho é onde vive a maior parte do código de aplicação — referências tipadas percorridas de forma eager em uma requisição, para os documentos relacionados chegarem juntos sem pipeline:

// JavaScript / Node.js — Back4app JS SDK
// A join in a document database: Pointer + include, one request
const query = new Parse.Query('Comment');
query.equalTo('post', postPointer);   // Comment.post is a Pointer<Post>
query.include('author');              // "join" the author document in
const comments = await query.find();

const name = comments[0].get('author').get('username'); // already loaded — no N+1

Embutir vs. referenciar: a decisão

Fluxo de decisão entre embutir e referenciarDados sempre lidos com o pai e de tamanho limitado devem ser embutidos; dados compartilhados, atualizados de forma independente ou sem limite de crescimento devem ser referenciados, com a direção da referência invertida para conjuntos enormes de filhos.

não

sim

não

sim

sim

não

sim

Sempre lido junto
com o pai?

Referenciar

Tamanho limitado?
(sem crescimento infinito)

Compartilhado com
outros pais?

Embutir

Quantidade enorme de filhos?

Inverta: guarde a referência
ao pai em cada filho

Dados sempre lidos com o pai e de tamanho limitado devem ser embutidos; dados compartilhados, atualizados de forma independente ou sem limite de crescimento devem ser referenciados, com a direção da referência invertida para conjuntos enormes de filhos.
DimensãoEmbutirReferenciar
Padrão de leituraSempre buscado com o paiBuscado sozinho ou sob demanda
Padrão de escritaAtualizado com o pai, atomicamenteAtualizado de forma independente
CardinalidadeUm-para-poucosUm-para-muitos em diante
CrescimentoLimitado (os endereços de uma pessoa)Sem limite (os comentários de um post)
CompartilhamentoPertence a um pai sóCompartilhado entre pais
O custo do joinZero — pago na escritaPago por consulta — lookup, include ou batch

Os degraus de cardinalidade dão a mesma tabela em forma de regra de bolso: um-para-poucos embute o array, um-para-muitos referencia por ID, um-para-enormes inverte a direção — o filho guarda a referência ao pai, porque nenhum documento pai pode segurar um array que só cresce. O que aponta para os dois antipadrões por trás da maioria dos incidentes de modelagem de documentos: arrays sem limite e o teto de 16MB por documento que eles acabam ameaçando. As correções padrão — referenciar, embutir um subconjunto limitado (os N mais recentes com uma coleção de excedente), ou agrupar filhos em documentos-balde — são todas versões de “pare o documento de crescer”.

O lookup, honestamente

O $lookup é um join de verdade com dois asteriscos honestos. Formato: ele retorna os casamentos como um array embutido por documento de entrada — $unwind o achata em linhas e, sem a semântica de preservar vazios, converte o left join em comportamento de inner join. Performance: motores de documentos só executam joins de loop aninhado, e os benchmarks públicos são diretos — juntar um milhão de documentos levou dezenas de segundos com índices, contra meio segundo do equivalente embutido. As regras operacionais que seguem daí: sempre indexe o campo estrangeiro (sem ele, cada documento de entrada dispara uma varredura da coleção), use lookups para joins por requisição sobre um punhado de documentos, e nunca construa fan-outs analíticos sobre eles — essa carga pertence a um warehouse ou a um motor relacional.

O problema N+1 e suas quatro saídas

O bug clássico: uma query para N pais, depois um loop disparando uma query por pai para os filhos — N+1 idas ao banco que escalam com o tamanho da página. As saídas, da melhor para as demais: batch — colete os IDs dos pais e busque todos os filhos numa query de contido-em (o populate dos bons ODMs faz isso por você); include — eager loading no nível do SDK, que retorna os documentos referenciados na mesma requisição, como nas abas acima; lookup — um pipeline no servidor; embutir — o join deixa de existir. O que transforma N+1 de bug em arquitetura é não notá-lo: ele entrega rápido com dez registros de teste e derrete com mil — a patologia completa tem verbete próprio no registro deste glossário.

Casos de uso comuns

  • Conteúdo com autoria. Posts, comentários, autores — referências com includes para as listas, embeds para os campos de snapshot só de exibição.
  • Catálogos e pedidos. A referência estendida canônica: um pedido embute o nome e o preço do produto como vendidos (snapshot imutável) mais uma referência ao produto vivo.
  • Fluxos de atividade e eventos. Um-para-enormes — documentos filhos guardando referências ao pai, nunca arrays no pai.
  • Perfis de usuário. A vitrine do embutir: endereços, preferências, configurações — lidos juntos, limitados, de um dono só.
  • Grafos sociais. Arrays de referências muitos-para-muitos — e o sinal honesto de que, passado um ponto, esse formato quer um motor relacional ou de grafos.

Embutir, referenciar ou trocar de motor? Matriz de decisão

Embuta quando…Referencie quando…Use um banco relacional quando…
Lido e atualizado juntoAcessado por conta própriaJoins são a carga de trabalho, não a exceção
Pequeno e limitadoSem limite ou de alta cardinalidadeAnalytics ad hoc cruzando entidades
De um pai sóCompartilhado entre paisIntegridade referencial estrita exigida
Atualização atômica com o pai importaCiclos de atualização independentesTransações complexas de múltiplas linhas
O caso clássico: campos de perfilO caso clássico: comentáriosO caso clássico: domínios pesados em muitos-para-muitos

A terceira coluna é a seção que páginas de fornecedor não escrevem: se toda tela precisa de três lookups e a integridade referencial tira o seu sono, os dados estão pedindo tabelas — modelos de documentos ganham quando os padrões de acesso são conhecidos e hierárquicos, não como substituto universal.

Limitações e trade-offs

  • Denormalização é um instrumento de dívida. Campos duplicados matam joins mas precisam ser quitados no update — escritas em fan-out, janelas de dados velhos e código de consistência (transações ou triggers de change stream) são os juros.
  • Sem chaves estrangeiras, sem rede de proteção. Referências não garantem existência; deletar um autor deixa referências de livros órfãs em silêncio, a menos que a plataforma ou o seu código limpe.
  • Lookups não otimizam. Sem reordenação de joins, sem estratégias de hash — a ordem do pipeline é o seu plano de query.
  • Migrações entre formatos são projetos de verdade. De embutido para referenciado (o resgate do array que cresceu) significa fazer backfill de coleções e reescrever queries — modele para a cardinalidade de amanhã, não a de hoje.
  • O teto de 16MB é um penhasco, não um aviso. Padrões de crescimento que se aproximam dele degradam a performance muito antes do impacto.

Consultas relacionais no Back4app

O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. Seu banco de documentos fala um vocabulário relacional por design: Pointers declaram arestas um-para-muitos, Relations resolvem muitos-para-muitos, e include() percorre as arestas numa única requisição — a saída do N+1 embutida no SDK, como as abas de código acima mostram. A API GraphQL gerada automaticamente aninha objetos relacionados em uma query de graça, e deletes podem cascatear por triggers de Cloud Code — a rede de proteção de integridade referencial que motores de documentos deixam de fora.

Perguntas frequentes

MongoDB suporta joins?

Não são joins no estilo SQL — mas são joins relacionalmente úteis. O estágio de agregação $lookup faz um left outer join, anexando os documentos correspondentes de outra coleção como um array embutido em vez de linhas planas. Adicionar $unwind converte para semântica de inner join. O que difere do SQL é o formato do resultado, o perfil de performance, e o fato de o join ser exceção, não padrão.

Devo embutir ou referenciar dados relacionados?

A regra de consenso: embuta o que você lê, atualiza e arquiva junto — dados pequenos, limitados e fortemente acoplados. Referencie o que vive por conta própria: compartilhado entre pais, atualizado de forma independente, de alta cardinalidade ou sem limite de crescimento. A orientação oficial manda favorecer embutir na ausência de um motivo forte — e os motivos fortes são exatamente esses quatro.

Como a cardinalidade muda a decisão de modelagem?

Os degraus clássicos: um-para-poucos (os endereços de uma pessoa) — embuta o array. Um-para-muitos (centenas a milhares) — um array de referências. Um-para-enormes (os eventos de log de uma máquina) — inverta a direção e guarde a referência ao pai em cada filho, porque o pai não pode segurar um array que só cresce. Muitos-para-muitos — arrays de referências, às vezes dos dois lados.

O $lookup é lento?

Comparado a joins relacionais, consistentemente sim: motores de documentos executam joins de loop aninhado, sem as estratégias de merge e hash que otimizadores relacionais têm. Benchmarks públicos juntando um milhão de documentos mediram dezenas de segundos mesmo com índice, contra meio segundo do equivalente embutido. As regras operacionais: sempre indexe o campo estrangeiro, mantenha o $lookup em caminhos por requisição juntando um punhado de documentos, e nunca construa fan-outs analíticos sobre ele.

O que é o limite de 16MB por documento e por que importa aqui?

Um teto rígido para o tamanho de um único documento — e o motivo pelo qual "embuta tudo" falha. Arrays embutidos sem limite (comentários, logs, eventos) crescem em direção ao teto e degradam cache e eficiência de índice muito antes de atingi-lo. As correções padrão: trocar para referências, embutir só um subconjunto limitado (os N mais recentes) com uma coleção de excedente, ou agrupar filhos em documentos-balde.

O que é o problema N+1 em bancos de documentos?

Buscar N pais com uma query e depois disparar mais uma query por pai para os dados relacionados — N+1 idas ao banco que crescem com o tamanho do resultado. As correções, na ordem de preferência: agrupar o segundo passo numa única query sobre os IDs coletados, usar um lookup no servidor num pipeline só, buscar os documentos relacionados numa requisição via o mecanismo de include do SDK, ou embutir para que o "join" tenha acontecido na escrita.

Como ODMs e SDKs de backend expressam relações?

Como referências tipadas com um operador de eager loading. ODMs de documentos declaram campos de referência e os populam com queries de acompanhamento em batch; SDKs de backend usam Pointers — uma referência tipada a outro objeto — e um operador include que busca os documentos referenciados na mesma requisição, mais tipos Relation para grandes conjuntos muitos-para-muitos. A mesma ideia em toda parte: declare a aresta, depois escolha quando percorrê-la.

Quando um banco relacional é simplesmente a escolha melhor?

Quando joins são a carga de trabalho, e não a exceção: domínios pesados em muitos-para-muitos, analytics ad hoc cruzando entidades, integridade referencial estrita e transações complexas de múltiplas linhas. Modelos de documentos ganham quando os padrões de acesso são conhecidos e hierárquicos — um documento por tela de dados. Se toda query precisa de três lookups, os dados estão dizendo que querem tabelas.

Termos relacionados

Compare com

Leitura adicional

Pronto para construir seu backend?

Comece seu projeto no Back4app em minutos — banco de dados, autenticação, APIs e Cloud Code incluídos. Sem cartão de crédito.

Escrito e revisado por Back4app Engineering, Back4app Engineering · Publicado em 2026-08-21