Relationale Abfragen (Joins) in Dokumentdatenbanken

Aktualisiert: September 2026

Eine relationale Abfrage in einer Dokumentdatenbank ist ein Join über Referenzen und Lookups statt Fremdschlüssel – oder wird durch Einbetten vermieden. Dieses “oder” ist das ganze Thema: Dokumentdatenbanken bieten drei Wege, Daten in Beziehung zu setzen, und die Entwurfsentscheidung betrifft den Zeitpunkt des Joins – beim Schreiben (Einbetten), zur Abfragezeit im Server (Lookup) oder zur Abfragezeit in der Anwendung (Referenzen plus ein zweiter Ladevorgang).

Das Wichtigste in Kürze

FrageAntwort
Können Dokument-DBs joinen?Ja – Lookups machen Left Outer Joins und liefern Arrays, keine Zeilen
Die eigentliche EntscheidungEinbetten vs. referenzieren – wann findet der Join statt?
StandardregelEinbetten, was zusammen gelesen wird und begrenzt ist; referenzieren, was für sich steht oder wächst
Die Wahrheit zur PerformanceServerseitige Lookups sind Nested-Loop-Joins – gut pro Anfrage, falsch für Analytik
Der klassische FehlerN+1-Abfragen – behoben durch Batching, Includes oder Einbetten

Die drei Wege, Dokumente zu verbinden

// 1 · Einbetten — der "Join" fand beim Schreiben statt
{ _id: 1, title: "Dune", author: { name: "Frank Herbert", born: 1920 } }

// 2 · Referenz + $lookup — der Join findet zur Abfragezeit statt, serverseitig
db.books.aggregate([
  { $lookup: { from: "authors", localField: "authorId",
               foreignField: "_id", as: "author" } },  // Left Outer Join → Array
  { $unwind: "$author" }                               // flach ziehen → Inner Join
])

// 3 · Referenz + Join in der Anwendung — der ODM/SDK-Weg (unten)

Der dritte Weg ist der, auf dem der meiste Anwendungscode lebt – typisierte Referenzen, in einer Anfrage vorausgeladen, sodass verwandte Dokumente ohne Pipeline gemeinsam ankommen:

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

Einbetten vs. Referenzieren: die Entscheidung

Entscheidungsfluss zwischen Einbetten und ReferenzierenDaten, die immer mit ihrem Elternobjekt gelesen werden und in der Größe begrenzt sind, sollten eingebettet werden; geteilte, unabhängig aktualisierte oder unbegrenzte Daten sollten referenziert werden, wobei die Richtung der Referenz bei riesigen Kindmengen umgedreht wird.

nein

ja

nein

ja

ja

nein

ja

Immer zusammen mit dem
Elternobjekt gelesen?

Referenzieren

Begrenzte Größe?
(kein endloses Wachstum)

Mit anderen
Elternobjekten geteilt?

Einbetten

Riesige Kindanzahl?

Umdrehen: die Eltern-Referenz
in jedem Kind speichern

Daten, die immer mit ihrem Elternobjekt gelesen werden und in der Größe begrenzt sind, sollten eingebettet werden; geteilte, unabhängig aktualisierte oder unbegrenzte Daten sollten referenziert werden, wobei die Richtung der Referenz bei riesigen Kindmengen umgedreht wird.
DimensionEinbettenReferenzieren
LesemusterImmer mit dem Elternobjekt geladenEigenständig oder bei Bedarf geladen
SchreibmusterAtomar mit dem Elternobjekt geändertUnabhängig geändert
KardinalitätEins-zu-wenigeEins-zu-viele und darüber hinaus
WachstumBegrenzt (die Adressen einer Person)Unbegrenzt (die Kommentare eines Beitrags)
TeilungGehört zu einem ElternobjektÜber Elternobjekte hinweg geteilt
Die Join-KostenNull – beim Schreiben bezahltPro Abfrage bezahlt – Lookup, Include oder Batch

Die Kardinalitätsstufen ergeben dieselbe Tabelle als Faustregeln: Eins-zu-wenige bettet das Array ein, Eins-zu-viele referenziert per ID, Eins-zu-riesig dreht die Richtung um – das Kind speichert die Referenz auf das Elternobjekt, weil kein Elterndokument ein unaufhörlich wachsendes Array halten kann. Damit sind auch die beiden Anti-Patterns benannt, die hinter den meisten Zwischenfällen bei der Dokumentmodellierung stehen: unbegrenzte Arrays und die 16-MB-Grenze pro Dokument, die sie irgendwann bedrohen. Die üblichen Auswege – stattdessen referenzieren, eine begrenzte Teilmenge einbetten (die neuesten N plus eine Überlauf-Collection) oder Kinder in gruppierte Dokumente bündeln – sind allesamt Varianten von “das Dokument am Wachsen hindern”.

Der Lookup, ehrlich betrachtet

$lookup ist ein echter Join mit zwei ehrlichen Sternchen. Zur Form: Er liefert Treffer als eingebettetes Array pro Eingabedokument – $unwind zieht es zu Zeilen flach und verwandelt ohne Preserve-Empty-Semantik das Left-Join- in Inner-Join-Verhalten. Zur Performance: Dokument-Engines führen ausschließlich Nested-Loop-Joins aus, und öffentliche Benchmarks sind unmissverständlich – der Join über eine Million Dokumente dauerte mit Indizes mehrere Dutzend Sekunden, gegenüber einer halben Sekunde für das eingebettete Äquivalent. Daraus folgen die Betriebsregeln: das Fremdfeld immer indizieren (ohne Index löst jedes Eingabedokument einen Collection-Scan aus), Lookups für Joins pro Anfrage über eine Handvoll Dokumente einsetzen und niemals analytische Fan-outs darauf bauen – diese Last gehört in ein Data Warehouse oder eine relationale Engine.

Das N+1-Problem und seine vier Auswege

Der klassische Fehler: eine Abfrage für N Elternobjekte, dann eine Schleife, die pro Elternobjekt eine Abfrage für dessen Kinder absetzt – N+1 Roundtrips, die mit der Seitengröße skalieren. Die Auswege, der beste zuerst: Batching – die Eltern-IDs sammeln und alle Kinder in einer “enthalten-in”-Abfrage holen (das populate guter ODMs erledigt das für Sie); Include – Eager Loading auf SDK-Ebene, das die referenzierten Dokumente in derselben Anfrage zurückgibt, wie in den Tabs oben; Lookup – eine serverseitige Pipeline; Einbetten – der Join hört auf zu existieren. Was N+1 vom Fehler zur Architektur macht, ist, es nicht zu bemerken: Auf zehn Testdatensätzen geht es glatt durch und bei tausend schmilzt es weg – die vollständige Pathologie hat einen eigenen Eintrag im Register dieses Glossars.

Typische Anwendungsfälle

  • Inhalte mit Urheberschaft. Beiträge, Kommentare, Autoren – Referenzen mit Includes für die Listenansichten, Einbettungen für die reinen Anzeige-Schnappschussfelder.
  • Kataloge und Bestellungen. Der kanonische Fall der erweiterten Referenz: Eine Bestellung bettet Produktname und Verkaufspreis ein (unveränderlicher Schnappschuss) und hält zusätzlich eine Referenz auf das lebende Produkt.
  • Aktivitäts- und Ereignisströme. Eins-zu-riesig – Kinddokumente halten Referenzen auf das Elternobjekt, niemals Arrays am Elternobjekt.
  • Benutzerprofile. Das Paradebeispiel fürs Einbetten: Adressen, Präferenzen, Einstellungen – zusammen gelesen, begrenzt, eindeutig zugehörig.
  • Soziale Graphen. Viele-zu-viele-Referenz-Arrays – und das ehrliche Signal, dass diese Form ab einem gewissen Punkt eine relationale oder eine Graph-Engine will.

Einbetten, referenzieren oder Engine wechseln? Eine Entscheidungsmatrix

Einbetten, wenn…Referenzieren, wenn…Relationale DB, wenn…
Zusammen gelesen und geändertEigenständig zugegriffenJoins die Arbeitslast sind, nicht die Ausnahme
Klein und begrenztUnbegrenzt oder hohe KardinalitätSpontane Auswertungen über Entitäten hinweg
Einem Elternobjekt zugehörigÜber Elternobjekte hinweg geteiltStrenge referenzielle Integrität gefordert
Atomare Updates mit dem Elternobjekt zählenUnabhängige Update-ZyklenKomplexe Transaktionen über viele Zeilen
Der klassische Fall: ProfilfelderDer klassische Fall: KommentareDer klassische Fall: Viele-zu-viele-lastige Domänen

Die dritte Spalte ist der Abschnitt, den Herstellerseiten nicht schreiben werden: Wenn jeder Bildschirm drei Lookups braucht und referenzielle Integrität Sie nachts wachhält, verlangen die Daten nach Tabellen – Dokumentmodelle gewinnen, wenn die Zugriffsmuster bekannt und hierarchisch sind, nicht als universeller Ersatz.

Grenzen und Trade-offs

  • Denormalisierung ist ein Schuldschein. Duplizierte Felder sparen Joins, müssen aber bei jedem Update zurückgezahlt werden – Fan-out-Schreibvorgänge, Fenster mit veralteten Daten und Konsistenzcode (Transaktionen oder Change-Stream-Trigger) sind die Zinsen.
  • Keine Fremdschlüssel, kein Sicherheitsnetz. Referenzen erzwingen keine Existenz; wer einen Autor löscht, hinterlässt stillschweigend verwaiste Buchreferenzen, solange Plattform oder Code nicht aufräumen.
  • Lookups optimieren nicht. Kein Umsortieren von Joins, keine Hash-Strategien – die Reihenfolge der Pipeline ist Ihr Abfrageplan.
  • Migrationen zwischen den Formen sind echte Projekte. Von eingebettet zu referenziert (die Rettung des wachsenden Arrays) heißt, Collections nachzufüllen und Abfragen neu zu schreiben – modellieren Sie für die Kardinalität von morgen, nicht die von heute.
  • Die 16-MB-Grenze ist eine Klippe, keine Warnung. Wachstumsmuster, die sich ihr nähern, verschlechtern die Performance lange, bevor sie sie erreichen.

Relationale Abfragen mit Back4app

Back4app ist eine Open-Source-Plattform für Backend as a Service (BaaS), die eine verwaltete Datenbank, automatisch generierte REST- und GraphQL-APIs, Authentifizierung, Dateispeicher und Serverless-Funktionen mit Cloud Code kombiniert. Ihre Dokumentdatenbank spricht von Haus aus ein relationales Vokabular: Pointers deklarieren Eins-zu-viele-Kanten, Relations übernehmen Viele-zu-viele, und include() geht die Kanten in einer einzigen Anfrage ab – der N+1-Ausweg, fest im SDK eingebaut, wie die Code-Tabs oben zeigen. Die automatisch generierte GraphQL-API verschachtelt verwandte Objekte gratis in einer Abfrage, und Löschungen können über Cloud-Code-Trigger kaskadieren – das Sicherheitsnetz für referenzielle Integrität, das Dokument-Engines auslassen.

Häufige Fragen

Unterstützt MongoDB Joins?

Keine Joins nach SQL-Art – aber ja, relational brauchbare. Die Aggregationsstufe $lookup führt einen Left Outer Join aus und hängt passende Dokumente einer anderen Collection als eingebettetes Array an statt als flache Zeilen. Ergänzt man $unwind, entsteht Inner-Join-Semantik. Anders als bei SQL sind die Form des Ergebnisses, das Performance-Profil und die Tatsache, dass Joins hier die Ausnahme und nicht der Normalfall sind.

Sollte ich verwandte Daten einbetten oder referenzieren?

Die Konsensregel: Betten Sie ein, was Sie zusammen lesen, ändern und archivieren – kleine, begrenzte, eng gekoppelte Daten. Referenzieren Sie, was für sich steht: über mehrere Elternobjekte geteilt, unabhängig aktualisiert, von hoher Kardinalität oder unbegrenzt. Die Herstellerempfehlung lautet, ohne zwingenden Grund das Einbetten zu bevorzugen – und die zwingenden Gründe sind genau diese vier.

Wie verändert die Kardinalität die Modellierungsentscheidung?

Die klassischen Stufen: Eins-zu-wenige (die Adressen einer Person) – das Array einbetten. Eins-zu-viele (Hunderte bis Tausende) – ein Array von Referenzen. Eins-zu-riesig (die Log-Ereignisse einer Maschine) – die Richtung umdrehen und die Referenz auf das Elternobjekt in jedem Kind speichern, denn das Elternobjekt kann kein unbegrenzt wachsendes Array halten. Viele-zu-viele – Arrays von Referenzen, manchmal auf beiden Seiten.

Ist $lookup langsam?

Im Vergleich zu relationalen Joins durchgängig ja: Dokument-Engines führen Nested-Loop-Joins aus, ohne die Merge- und Hash-Strategien relationaler Optimierer. Öffentliche Benchmarks maßen beim Join über eine Million Dokumente selbst mit Index mehrere Dutzend Sekunden gegenüber einer halben Sekunde für das eingebettete Äquivalent. Die Betriebsregeln: das Fremdfeld immer indizieren, $lookup nur auf Pfaden pro Anfrage einsetzen, die eine Handvoll Dokumente verbinden, und darauf niemals analytische Fan-outs bauen.

Was ist die 16-MB-Grenze und warum ist sie hier wichtig?

Eine harte Obergrenze für die Größe eines einzelnen Dokuments – und der Grund, warum "einfach alles einbetten" scheitert. Unbegrenzte eingebettete Arrays (Kommentare, Logs, Ereignisse) wachsen auf die Grenze zu und verschlechtern die Effizienz von Cache und Index lange, bevor sie erreicht ist. Die üblichen Auswege: auf Referenzen umstellen, nur eine begrenzte Teilmenge einbetten (die neuesten N) und den Rest in eine Überlauf-Collection legen, oder Kinder in gruppierte Dokumente bündeln.

Was ist das N+1-Problem in Dokumentdatenbanken?

N Elternobjekte mit einer Abfrage holen und dann pro Elternobjekt eine weitere Abfrage für dessen verwandte Daten absetzen – N+1 Roundtrips, die mit der Ergebnismenge wachsen. Die Auswege, nach Vorzug geordnet: den zweiten Schritt zu einer einzigen Abfrage über die gesammelten IDs bündeln, ein serverseitiges Lookup in einer Pipeline verwenden, verwandte Dokumente über den Include-Mechanismus des SDK in einer Anfrage holen, oder einbetten, sodass der "Join" schon beim Schreiben passiert ist.

Wie drücken ODMs und Backend-SDKs Relationen aus?

Als typisierte Referenzen mit einem Operator für Eager Loading. Dokument-ODMs deklarieren Referenzfelder und füllen sie mit gebündelten Folgeabfragen; Backend-SDKs nutzen Pointers – eine typisierte Referenz auf ein anderes Objekt – und einen Include-Operator, der die referenzierten Dokumente in derselben Anfrage lädt, dazu Relation-Typen für große Viele-zu-viele-Mengen. Überall dieselbe Idee: die Kante deklarieren und dann entscheiden, wann man sie geht.

Wann ist eine relationale Datenbank schlicht die bessere Wahl?

Wenn Joins die Arbeitslast sind statt die Ausnahme: Viele-zu-viele-lastige Domänen, spontane Auswertungen über Entitäten hinweg, strenge referenzielle Integrität und komplexe Transaktionen über viele Zeilen. Dokumentmodelle gewinnen, wenn die Zugriffsmuster bekannt und hierarchisch sind – ein Dokument pro Bildschirm voller Daten. Wenn jede Abfrage drei Lookups braucht, sagen Ihnen die Daten, dass sie Tabellen wollen.

Verwandte Begriffe

Vergleichen mit

Weiterführende Quellen

Bereit, Ihr Backend zu bauen?

Starten Sie Ihr Projekt auf Back4app in wenigen Minuten — Datenbank, Authentifizierung, APIs und Cloud Code inklusive. Keine Kreditkarte erforderlich.

Geschrieben und geprüft von Back4app Engineering, Back4app Engineering · Veröffentlicht am 2026-09-24