---
term: 'Consultas Relacionais (Joins) em Bancos de Documentos'
seoTitle: 'Joins em Bancos de Documentos: Embutir, Referenciar e Lookups'
headline: 'Como funcionam joins em bancos de documentos?'
slug: joins-em-bancos-de-documentos
category: database
shortDefinition: '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.'
relatedTerms:
  - n-plus-one-query-problem
  - database-abstraction-layer
  - auto-generated-database-apis
  - overfetching-underfetching
  - data-modeling
contrastsWith:
  - n-plus-one-query-problem
faq:
  - question: 'MongoDB suporta joins?'
    answer: '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.'
  - question: 'Devo embutir ou referenciar dados relacionados?'
    answer: '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.'
  - question: 'Como a cardinalidade muda a decisão de modelagem?'
    answer: '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.'
  - question: 'O $lookup é lento?'
    answer: '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.'
  - question: 'O que é o limite de 16MB por documento e por que importa aqui?'
    answer: '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.'
  - question: 'O que é o problema N+1 em bancos de documentos?'
    answer: '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.'
  - question: 'Como ODMs e SDKs de backend expressam relações?'
    answer: '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.'
  - question: 'Quando um banco relacional é simplesmente a escolha melhor?'
    answer: '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.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'MongoDB $lookup aggregation reference'
    url: 'https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/'
  - name: 'MongoDB — Embedding vs. References'
    url: 'https://www.mongodb.com/docs/manual/data-modeling/concepts/embedding-vs-references/'
  - name: 'Mongoose populate documentation'
    url: 'https://mongoosejs.com/docs/populate.html'
  - name: 'SDK relational data guide'
    url: 'https://docs.parseplatform.org/js/guide/#relational-data'
cta:
  title: 'Joins sem dor de cabeça de modelagem'
  text: 'O Back4app dá aos dados de documento um vocabulário relacional: Pointers e Relations declaram as arestas, include() as percorre em uma requisição, e a API GraphQL gerada automaticamente aninha objetos relacionados numa única query. Sem N+1, sem pipeline de agregação para ajustar à mão.'
  linkText: 'Comece grátis'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-08-21'
translationKey: relational-queries-document-databases
---

**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

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

## As três formas de relacionar documentos

```javascript
// 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:**

```javascript
// 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
```

**Flutter:**

```dart
// Flutter / Dart — Back4app Flutter SDK
// A join in a document database: Pointer + include, one request
final query = QueryBuilder<ParseObject>(ParseObject('Comment'))
  ..whereEqualTo('post', postPointer)
  ..includeObject(['author']); // "join" the author document in
final response = await query.query();

final author = (response.results!.first as ParseObject)
    .get<ParseObject>('author'); // already loaded — no N+1
```

**Swift:**

```swift
// iOS / Swift — Back4app Swift SDK
// A join in a document database: Pointer + include, one request
let query = Comment.query("post" == postPointer)
  .include("author") // "join" the author document in
query.find { result in
  if case .success(let comments) = result {
    print(comments.first?.author?.username ?? "") // already loaded — no N+1
  }
}
```

**Kotlin:**

```kotlin
// Android / Kotlin — Back4app Android SDK
// A join in a document database: Pointer + include, one request
val query = ParseQuery.getQuery<ParseObject>("Comment")
query.whereEqualTo("post", postPointer)
query.include("author") // "join" the author document in
query.findInBackground { comments, e ->
  if (e == null) {
    val name = comments[0].getParseObject("author")?.getString("username")
    Log.d("Comments", "by $name") // already loaded — no N+1
  }
}
```

## Embutir vs. referenciar: a decisão

```mermaid
flowchart TB
  accTitle: Fluxo de decisão entre embutir e referenciar
  accDescr: 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.
  A{"Sempre lido junto<br/>com o pai?"} -- não --> R["Referenciar"]
  A -- sim --> B{"Tamanho limitado?<br/>(sem crescimento infinito)"}
  B -- não --> R
  B -- sim --> C{"Compartilhado com<br/>outros pais?"}
  C -- sim --> R
  C -- não --> E["Embutir"]
  R --> D{"Quantidade enorme de filhos?"}
  D -- sim --> F["Inverta: guarde a referência<br/>ao pai em cada filho"]
```

| Dimensão | Embutir | Referenciar |
| --- | --- | --- |
| Padrão de leitura | Sempre buscado com o pai | Buscado sozinho ou sob demanda |
| Padrão de escrita | Atualizado com o pai, atomicamente | Atualizado de forma independente |
| Cardinalidade | Um-para-poucos | Um-para-muitos em diante |
| Crescimento | Limitado (os endereços de uma pessoa) | Sem limite (os comentários de um post) |
| Compartilhamento | Pertence a um pai só | Compartilhado entre pais |
| O custo do join | Zero — pago na escrita | Pago 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`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/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 junto | Acessado por conta própria | Joins são a carga de trabalho, não a exceção |
| Pequeno e limitado | Sem limite ou de alta cardinalidade | Analytics ad hoc cruzando entidades |
| De um pai só | Compartilhado entre pais | Integridade referencial estrita exigida |
| Atualização atômica com o pai importa | Ciclos de atualização independentes | Transações complexas de múltiplas linhas |
| O caso clássico: campos de perfil | O caso clássico: comentários | O 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](https://docs.parseplatform.org/js/guide/#relational-data), 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.
