---
term: 'Query relazionali (join) nei database documentali'
seoTitle: 'Join nei database documentali: incorporare e riferimenti'
headline: 'Query relazionali (join) nei database documentali'
slug: join-nei-database-documentali
category: database
shortDefinition: 'Una query relazionale in un database documentale è un join fatto con riferimenti e lookup invece che con chiavi esterne – o evitato incorporando.'
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 supporta i join?'
    answer: 'Non i join in stile SQL – ma sì, join utili dal punto di vista relazionale. La fase di aggregazione $lookup esegue un left outer join, attaccando i documenti corrispondenti di un''altra collezione come array incorporato invece che come righe piatte. Aggiungere $unwind la porta a una semantica di inner join. Ciò che cambia rispetto a SQL è la forma del risultato, il profilo di prestazioni e il fatto che qui i join sono l''eccezione, non la regola.'
  - question: 'Conviene incorporare o referenziare i dati collegati?'
    answer: 'La regola di consenso: incorporare ciò che si legge, si aggiorna e si archivia insieme – dati piccoli, limitati, strettamente accoppiati. Referenziare ciò che vive da sé: condiviso tra più genitori, aggiornato in modo indipendente, a cardinalità alta oppure senza limiti. Le raccomandazioni ufficiali dicono di privilegiare l''incorporamento salvo ragioni stringenti, e le ragioni stringenti sono precisamente quelle quattro.'
  - question: 'Come cambia la cardinalità la decisione di modellazione?'
    answer: 'I livelli classici: uno-a-pochi (gli indirizzi di una persona) – si incorpora l''array. Uno-a-molti (da centinaia a migliaia) – un array di riferimenti. Uno-a-enormità (gli eventi di log di una macchina) – si inverte il senso e si archivia il riferimento al genitore in ogni figlio, perché il genitore non può contenere un array che cresce senza fine. Molti-a-molti – array di riferimenti, a volte su entrambi i lati.'
  - question: '$lookup è lento?'
    answer: 'Rispetto ai join relazionali, sì, in modo costante: i motori documentali eseguono join a cicli annidati (nested loop), senza le strategie merge e hash di cui dispongono gli ottimizzatori relazionali. Benchmark pubblici che uniscono un milione di documenti hanno misurato decine di secondi anche con gli indici, contro mezzo secondo per l''equivalente incorporato. Le regole operative: indicizzare sempre il campo esterno, riservare $lookup ai percorsi per richiesta che uniscono una manciata di documenti, e non costruirci mai sopra fan-out analitici.'
  - question: 'Che cos''è il limite di 16 MB per documento e perché conta qui?'
    answer: 'Un tetto rigido sulla dimensione di un singolo documento – e la ragione per cui "incorporare tutto e basta" non funziona. Gli array incorporati senza limiti (commenti, log, eventi) crescono verso quel tetto e degradano l''efficienza di cache e indici molto prima di raggiungerlo. I rimedi standard: passare ai riferimenti, incorporare solo un sottoinsieme limitato (gli N più recenti) con una collezione di overflow, oppure raggruppare i figli in documenti-bucket.'
  - question: 'Che cos''è il problema N+1 nei database documentali?'
    answer: 'Recuperare N genitori con una query, poi emettere una query in più per ogni genitore per i suoi dati collegati – N+1 round trip che crescono con l''insieme dei risultati. I rimedi, in ordine di preferenza: raggruppare il secondo passo in una sola query sugli ID raccolti, usare un lookup lato server in una sola pipeline, recuperare i documenti collegati in una sola richiesta tramite il meccanismo include dell''SDK, oppure incorporare perché il "join" sia già avvenuto in scrittura.'
  - question: 'Come esprimono le relazioni gli ODM e gli SDK di backend?'
    answer: 'Come riferimenti tipizzati con un operatore di caricamento anticipato (eager loading). Gli ODM documentali dichiarano campi di riferimento e li popolano con query di seguito raggruppate; gli SDK di backend usano i Pointer – un riferimento tipizzato a un altro oggetto – e un operatore include che recupera i documenti referenziati nella stessa richiesta, più i tipi Relation per i grandi insiemi molti-a-molti. La stessa idea in ogni caso: dichiarare l''arco, poi scegliere quando percorrerlo.'
  - question: 'Quando un database relazionale è semplicemente la scelta migliore?'
    answer: 'Quando i join sono il carico di lavoro invece dell''eccezione: domini carichi di molti-a-molti, analytics ad hoc tra entità, integrità referenziale stretta e transazioni complesse su più righe. I modelli documentali vincono quando i pattern di accesso sono noti e gerarchici – un documento per schermata di dati. Se ogni query richiede tre lookup, i dati stanno dicendo che vogliono delle tabelle.'
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: 'Join senza il mal di testa della modellazione'
  text: 'Back4app dà ai dati documentali un vocabolario relazionale: Pointer e Relation dichiarano gli archi, include() li percorre in una sola richiesta e l''API GraphQL generata automaticamente annida gli oggetti collegati in una sola query. Nessun N+1, nessuna pipeline di aggregazione da regolare a mano.'
  linkText: 'Inizia gratis'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-10-09'
translationKey: relational-queries-document-databases
---

**Una query relazionale in un database documentale è un join fatto con riferimenti e lookup invece che con chiavi esterne – o evitato incorporando.** Quella "o" è tutto l'argomento: i database documentali offrono tre modi di collegare i dati, e la decisione di progetto riguarda *quando* avviene il join – in scrittura (incorporamento), in query dentro il server (lookup) oppure in query dentro l'applicazione (riferimenti più una lettura successiva).

## Punti chiave

| Domanda | Risposta |
| --- | --- |
| I database documentali sanno fare join? | Sì – i lookup fanno left outer join e restituiscono array, non righe |
| La vera decisione | Incorporare vs. referenziare – quando avviene il join? |
| Regola predefinita | Incorporare ciò che si legge insieme ed è limitato; referenziare ciò che vive da sé o cresce |
| La verità sulle prestazioni | I lookup lato server sono join a cicli annidati – adatti per richiesta, sbagliati per l'analytics |
| Il bug classico | Query N+1 – si risolve con batching, include o incorporamento |

## I tre modi di collegare i documenti

```javascript
// 1 · Incorporare – il "join" è avvenuto in scrittura
{ _id: 1, title: "Dune", author: { name: "Frank Herbert", born: 1920 } }

// 2 · Riferimento + $lookup – il join avviene in query, lato server
db.books.aggregate([
  { $lookup: { from: "authors", localField: "authorId",
               foreignField: "_id", as: "author" } },  // left outer join → array
  { $unwind: "$author" }                               // appiattisce → inner join
])

// 3 · Riferimento + join lato applicazione – la via ODM/SDK (qui sotto)
```

La terza strada è dove vive quasi tutto il codice applicativo – riferimenti tipizzati percorsi in modo anticipato in una sola richiesta, così i documenti collegati arrivano insieme senza una 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
  }
}
```

## Incorporare vs. referenziare: la decisione

```mermaid
flowchart TB
  accTitle: Flusso decisionale tra incorporare e referenziare
  accDescr: I dati sempre letti insieme al genitore e limitati nella dimensione vanno incorporati; i dati condivisi, aggiornati in modo indipendente o senza limiti vanno referenziati, invertendo la direzione del riferimento quando gli insiemi di figli sono enormi.
  A{"Letti sempre insieme<br/>al genitore?"} -- no --> R["Referenziare"]
  A -- sì --> B{"Dimensione limitata?<br/>(nessuna crescita infinita)"}
  B -- no --> R
  B -- sì --> C{"Condivisi con<br/>altri genitori?"}
  C -- sì --> R
  C -- no --> E["Incorporare"]
  R --> D{"Numero di figli enorme?"}
  D -- sì --> F["Invertire: il riferimento al<br/>genitore in ogni figlio"]
```

| Dimensione | Incorporare | Referenziare |
| --- | --- | --- |
| Pattern di lettura | Sempre recuperati con il genitore | Recuperati a sé o su richiesta |
| Pattern di scrittura | Aggiornati con il genitore, in modo atomico | Aggiornati in modo indipendente |
| Cardinalità | Uno-a-pochi | Uno-a-molti e oltre |
| Crescita | Limitata (gli indirizzi di una persona) | Senza limiti (i commenti di un post) |
| Condivisione | Appartiene a un solo genitore | Condiviso tra più genitori |
| Il costo del join | Zero – pagato in scrittura | Pagato per query – lookup, include o batch |

I livelli di cardinalità danno la stessa tabella sotto forma di regole empiriche: **uno-a-pochi** incorpora l'array, **uno-a-molti** referenzia per ID, **uno-a-enormità** inverte il senso – il figlio archivia il riferimento al genitore, perché nessun documento genitore può contenere un array che cresce senza fine. Il che punta ai due anti-pattern dietro la maggior parte degli incidenti di modellazione documentale: gli **array senza limiti** e il **tetto di 16 MB per documento** che alla fine minacciano. I rimedi standard – referenziare, incorporare un sottoinsieme limitato (gli N più recenti con una collezione di overflow) oppure raggruppare i figli in documenti-bucket – sono tutti varianti di "fermare la crescita del documento".

## Il lookup, onestamente

[`$lookup`](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/) è un vero join con due asterischi onesti. Forma: restituisce le corrispondenze come *array incorporato* per ogni documento in ingresso – `$unwind` lo appiattisce in righe e, senza la semantica di conservazione dei vuoti, converte il comportamento da left join a inner join. Prestazioni: i motori documentali eseguono soltanto join a cicli annidati, e i benchmark pubblici sono spietati – unire un milione di documenti ha richiesto decine di secondi *con* gli indici, contro mezzo secondo per l'equivalente incorporato. Le regole operative che ne derivano: indicizzare sempre il campo esterno (senza, ogni documento in ingresso innesca una scansione della collezione), usare i lookup per join per singola richiesta su una manciata di documenti, e non costruirci mai sopra fan-out analitici – quel carico di lavoro spetta a un data warehouse o a un motore relazionale.

## Il problema N+1 e le sue quattro vie d'uscita

Il bug classico: una query per N genitori, poi un ciclo che emette una query per ciascun genitore per i suoi figli – N+1 round trip che crescono con la dimensione della pagina. Le vie d'uscita, dalla migliore: **batch** – raccogliere gli ID dei genitori e recuperare tutti i figli con una sola query di appartenenza a un insieme (il populate dei buoni ODM lo fa da sé); **include** – eager loading a livello di SDK che restituisce i documenti referenziati nella stessa richiesta, come nei tab qui sopra; **lookup** – una sola pipeline lato server; **incorporare** – il join smette di esistere. Ciò che trasforma N+1 da bug in architettura è non accorgersene: passa in fretta su dieci record di test e fonde su mille – la patologia completa ha una voce propria nel registro di questo glossario.

## Casi d'uso tipici

- **Contenuti con autore.** Post, commenti, autori – riferimenti con include per le viste a elenco, incorporamenti per i campi-istantanea di sola visualizzazione.
- **Cataloghi e ordini.** Il caso canonico del *riferimento esteso*: un ordine incorpora nome e prezzo del prodotto come venduto (istantanea immutabile) più un riferimento al prodotto vivo.
- **Flussi di attività ed eventi.** Uno-a-enormità – documenti figli che portano i riferimenti al genitore, mai array sul genitore.
- **Profili utente.** La vetrina dell'incorporamento: indirizzi, preferenze, impostazioni – letti insieme, limitati, posseduti.
- **Grafi sociali.** Array di riferimenti molti-a-molti – e il segnale onesto che, oltre un certo punto, questa forma vuole un motore relazionale o a grafo.

## Incorporare, referenziare o cambiare motore? Matrice decisionale

| Incorporare quando… | Referenziare quando… | Usare un database relazionale quando… |
| --- | --- | --- |
| Letti e aggiornati insieme | Si accede a essi per conto proprio | I join sono il carico di lavoro, non l'eccezione |
| Piccoli e limitati | Senza limiti o a cardinalità alta | Serve analytics ad hoc tra entità |
| Posseduti da un solo genitore | Condivisi tra più genitori | Serve un'integrità referenziale stretta |
| Contano gli aggiornamenti atomici con il genitore | Cicli di aggiornamento indipendenti | Servono transazioni complesse su più righe |
| Il caso classico: i campi di profilo | Il caso classico: i commenti | Il caso classico: domini carichi di molti-a-molti |

La terza colonna è la sezione che le pagine dei fornitori non scriveranno: se ogni schermata richiede tre lookup e l'integrità referenziale toglie il sonno, i dati stanno chiedendo tabelle – i modelli documentali vincono quando i pattern di accesso sono noti e gerarchici, non come sostituto universale.

## Limiti e trade-off

- **La denormalizzazione è uno strumento di debito.** I campi duplicati eliminano i join, ma vanno ripagati a ogni aggiornamento – scritture a fan-out, finestre di dati obsoleti e codice di coerenza (transazioni o trigger su change stream) sono gli interessi.
- **Niente chiavi esterne, niente rete di sicurezza.** I riferimenti non impongono l'esistenza; cancellare un autore lascia orfani in silenzio i riferimenti dei libri, a meno che la piattaforma o il codice non facciano pulizia.
- **I lookup non ottimizzano.** Nessun riordino dei join, nessuna strategia hash – l'ordine della pipeline è il piano di esecuzione.
- **Le migrazioni tra forme sono progetti veri.** Passare da incorporato a referenziato (il salvataggio dell'array che cresce) significa riempire a posteriori nuove collezioni e riscrivere le query – conviene modellare per la cardinalità di domani, non per quella di oggi.
- **Il tetto di 16 MB è un salto nel vuoto, non un avviso.** I pattern di crescita che gli si avvicinano degradano le prestazioni molto prima di raggiungerlo.

## Le query relazionali su Back4app

Back4app è una piattaforma open-source di Backend as a Service (BaaS) che combina database gestito, API REST e GraphQL generate automaticamente, autenticazione, archiviazione di file e funzioni serverless con Cloud Code. Il suo database documentale parla un vocabolario relazionale per scelta progettuale: [i Pointer dichiarano gli archi uno-a-molti, le Relation gestiscono il molti-a-molti](https://docs.parseplatform.org/js/guide/#relational-data), e `include()` percorre gli archi in una sola richiesta – la via d'uscita da N+1 integrata nell'SDK, come mostrano i tab di codice qui sopra. L'API GraphQL generata automaticamente annida gli oggetti collegati in una sola query senza costi aggiuntivi, e le cancellazioni possono propagarsi a cascata tramite i trigger di Cloud Code – la rete di sicurezza per l'integrità referenziale che i motori documentali tralasciano.
