Hintergrund
Pipeline-Vorgänge sind eine neue Abfrageschnittstelle für Cloud Firestore. Diese Schnittstelle bietet erweiterte Abfragefunktionen, einschließlich komplexer Ausdrücke. Die Firestore Enterprise-Edition unterstützt relationale Joins durch korrelierte Unterabfragen. Im Gegensatz zu vielen NoSQL-Datenbanken, bei denen häufig Daten denormalisiert oder mehrere clientseitige Anfragen ausgeführt werden müssen, können Sie mit Unterabfragen Daten aus verknüpften Sammlungen oder Unterabfragen direkt auf dem Server kombinieren und aggregieren.
Unterabfragen sind Ausdrücke, die für jedes Dokument, das von der äußeren Abfrage verarbeitet wird, eine verschachtelte Pipeline ausführen. Dies ermöglicht komplexe Muster zum Abrufen von Daten, z. B. das Abrufen eines Dokuments zusammen mit den zugehörigen Unterabfrageelementen oder das Verknüpfen von logisch verknüpften Daten aus verschiedenen Stammsammlungen.
Konzepte
In diesem Abschnitt werden die wichtigsten Konzepte für die Verwendung von Unterabfragen zum Ausführen von Joins in Pipeline-Vorgängen vorgestellt.
Unterabfragen als Ausdrücke
Eine Unterabfrage ist keine Phase der obersten Ebene, sondern ein Ausdruck, der
in jeder Phase verwendet werden kann, die Ausdrücke akzeptiert, z. B.
select(...),
add_fields(...),
where(...) oder sort(...).
Cloud Firestore unterstützt drei Arten von Unterabfragen:
- Array-Unterabfragen:Das gesamte Ergebnis der Unterabfrage wird als Array von Dokumenten materialisiert.
- Skalare Unterabfragen:Sie werden zu einem einzelnen Wert ausgewertet, z. B. einer Anzahl, einem Durchschnitt oder einem bestimmten Feld aus einem verknüpften Dokument.
subcollection(...)-Unterabfragen : Vereinfachte Joins für eine 1:n-Beziehung zwischen über- und untergeordneten Elementen.
Umfang und Variablen
Beim Schreiben eines Joins muss die verschachtelte Unterabfrage häufig auf Felder aus dem „äußeren“ Dokument (dem übergeordneten Element) verweisen. Um diese Bereiche zu überbrücken, verwenden Sie die
let(...) Phase (in einigen
SDKs als define(...) bezeichnet), um Variablen im übergeordneten Bereich zu definieren, auf die dann in der
Unterabfrage mit der Funktion variable(...) verwiesen werden kann.
Syntax
In den folgenden Abschnitten erhalten Sie einen Überblick über die Syntax zum Ausführen von Joins.
Die Phase let(...)
Die let(...) Phase (in einigen
SDKs als define(...) bezeichnet) ist eine nicht filternde Phase, die Daten aus dem übergeordneten Bereich
explizit in eine benannte Variable für die Verwendung in nachfolgenden verschachtelten Bereichen überträgt.
Web
async function defineStageData() {
await setDoc(doc(collection(db, "Authors"), "author_123"), {
"id": "author_123",
"name": "Jane Austen"
});
}
Swift
func defineStageData() async throws { try await db.collection("authors").document("author_123").setData([ "id": "author_123", "name": "Jane Austen" ]) }
Kotlin
fun defineStageData() { val author = hashMapOf( "id" to "author_123", "name" to "Jane Austen", ) db.collection("Authors").document("author_123").set(author) }
Java
public void defineStageData() { Map<String, Object> author = new HashMap<>(); author.put("id", "author_123"); author.put("name", "Jane Austen"); db.collection("Authors").document("author_123").set(author); }
Array-Unterabfragen
Eine Array-Unterabfrage ist ein Sonderfall der Ausdruck-Unterabfrage, bei der das gesamte Ergebnis der Unterabfrage in einem Array materialisiert wird. Wenn die Unterabfrage keine Zeilen zurückgibt, wird sie zu einem leeren Array ausgewertet. Sie gibt niemals ein null-Array zurück. Solche Abfragen sind nützlich, wenn die vollständigen Ergebnisse im Endergebnis erforderlich sind, z. B. beim Materialisieren einer verschachtelten oder korrelierten Sammlung.
Abfragen können in der Unterabfrage gefiltert, sortiert und aggregiert werden, um die Menge der abzurufenden und zurückzugebenden Daten zu reduzieren und so die Kosten der Abfrage zu senken. Die Reihenfolge der Unterabfrage wird berücksichtigt. Das bedeutet, dass eine sort(...)-Phase in der Unterabfrage die Reihenfolge der Ergebnisse im endgültigen Array steuert.
Verwenden Sie den SDK-Wrapper toArrayExpression(), um eine Abfrage in ein Array zu konvertieren.
Web
async function toArrayExpressionStageData() {
await setDoc(doc(collection(db, "Projects"), "project_1"), {
"id": "project_1",
"name": "Alpha Build"
});
await addDoc(collection(db, "Tasks"), {
"project_id": "project_1",
"title": "System Architecture"
});
await addDoc(collection(db, "Tasks"), {
"project_id": "project_1",
"title": "Database Schema Design"
});
}
Antwort
{
id: "project_1",
name: "Alpha Build",
taskTitles: [
"System Architecture", "Database Schema Design"
]
}
Swift
async function toArrayExpressionStageData() { await setDoc(doc(collection(db, "Projects"), "project_1"), { "id": "project_1", "name": "Alpha Build" }); await addDoc(collection(db, "Tasks"), { "project_id": "project_1", "title": "System Architecture" }); await addDoc(collection(db, "Tasks"), { "project_id": "project_1", "title": "Database Schema Design" }); }
Antwort
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Kotlin
fun toArrayExpressionData() { val project = hashMapOf( "id" to "project_1", "name" to "Alpha Build", ) db.collection("Projects").document("project_1").set(project) val task1 = hashMapOf( "project_id" to "project_1", "title" to "System Architecture", ) db.collection("Tasks").add(task1) val task2 = hashMapOf( "project_id" to "project_1", "title" to "Database Schema Design", ) db.collection("Tasks").add(task2) }
Antwort
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Java
public void toArrayExpressionData() { Map<String, Object> project = new HashMap<>(); project.put("id", "project_1"); project.put("name", "Alpha Build"); db.collection("Projects").document("project_1").set(project); Map<String, Object> task1 = new HashMap<>(); task1.put("project_id", "project_1"); task1.put("title", "System Architecture"); db.collection("Tasks").add(task1); Map<String, Object> task2 = new HashMap<>(); task2.put("project_id", "project_1"); task2.put("title", "Database Schema Design"); db.collection("Tasks").add(task2); }
Antwort
{ id: "project_1", name: "Alpha Build", taskTitles: [ "System Architecture", "Database Schema Design" ] }
Skalare Unterabfragen
Skalare Unterabfragen werden häufig in einer select(...) oder
where(...) Phase verwendet, um das
Ergebnis einer Unterabfrage zu filtern oder zu erhalten, ohne die vollständige Abfrage direkt zu materialisieren.
Eine skalare Unterabfrage, die keine Ergebnisse liefert, wird selbst zu null ausgewertet. Eine Unterabfrage, die zu mehreren Elementen ausgewertet wird, führt zu einem Laufzeitfehler.
Wenn eine skalare Unterabfrage nur ein einzelnes Feld pro Ergebnis erzeugt, wird das Feld heraufgestuft und ist das Ergebnis der obersten Ebene für die Unterabfrage. Dies ist am häufigsten
der Fall, wenn die Unterabfrage mit einem select(field("user_name")) oder
aggregate(countAll().as("total")) endet und das Schema der Unterabfrage nur ein
einzelnes Feld enthält. Andernfalls werden mehrere Felder, die von einer Unterabfrage erzeugt werden können, in einer Map zusammengefasst.
Verwenden Sie den SDK-Wrapper toScalarExpression(), um eine Abfrage in einen skalaren Ausdruck zu konvertieren.
Web
async function toScalarExpressionStageData() {
await setDoc(doc(collection(db, "Authors"), "author_202"), {
"id": "author_202",
"name": "Charles Dickens"
});
await addDoc(collection(db, "Books"), {
"author_id": "author_202",
"title": "Great Expectations",
"rating": 4.8
});
await addDoc(collection(db, "Books"), {
"author_id": "author_202",
"title": "Oliver Twist",
"rating": 4.5
});
}
Antwort
{
"id": "author_202",
"name": "Charles Dickens",
"averageBookRating": 4.65
}
Swift
try await db.collection("authors").document("author_202").setData([ "id": "author_202", "name": "Charles Dickens" ]) try await db.collection("books").document().setData([ "author_id": "author_202", "title": "Great Expectations", "rating": 4.8 ]) try await db.collection("books").document().setData([ "author_id": "author_202", "title": "Oliver Twist", "rating": 4.5 ])
Antwort
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
Kotlin
fun toScalarExpressionData() { val author = hashMapOf( "id" to "author_202", "name" to "Charles Dickens", ) db.collection("Authors").document("author_202").set(author) val book1 = hashMapOf( "author_id" to "author_202", "title" to "Great Expectations", "rating" to 4.8, ) db.collection("Books").add(book1) val book2 = hashMapOf( "author_id" to "author_202", "title" to "Oliver Twist", "rating" to 4.5, ) db.collection("Books").add(book2) }
Antwort
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
Java
public void toScalarExpressionData() { Map<String, Object> author = new HashMap<>(); author.put("id", "author_202"); author.put("name", "Charles Dickens"); db.collection("Authors").document("author_202").set(author); Map<String, Object> book1 = new HashMap<>(); book1.put("author_id", "author_202"); book1.put("title", "Great Expectations"); book1.put("rating", 4.8); db.collection("Books").add(book1); Map<String, Object> book2 = new HashMap<>(); book2.put("author_id", "author_202"); book2.put("title", "Oliver Twist"); book2.put("rating", 4.5); db.collection("Books").add(book2); }
Antwort
{ "id": "author_202", "name": "Charles Dickens", "averageBookRating": 4.65 }
subcollection(...)-Unterabfragen
Die Eingabephase
subcollection(...) wird als Phase angeboten und ermöglicht
Joins für das hierarchische Datenmodell von Cloud Firestore. In einem hierarchischen Modell müssen Abfragen häufig ein Dokument zusammen mit Daten aus den zugehörigen Unterabfragen abrufen. Sie können dies mit einer
collection_group(...)-Eingabephase und einem Filter für die übergeordnete Referenz erreichen, aber subcollection(...) bietet eine viel präzisere Syntax.
Abgesehen von der impliziten Join-Bedingung verhält sich dies ähnlich wie eine Array-Unterabfrage. Es wird ein leeres Ergebnis zurückgegeben, wenn keine Dokumente gefunden werden, auch wenn die verschachtelte Sammlung nicht vorhanden ist.
Es handelt sich im Grunde um syntaktischen Zucker: Der __name__ des Dokuments im äußeren Bereich wird automatisch als Join-Schlüssel verwendet, um die hierarchische Beziehung aufzulösen. Daher ist dies die bevorzugte Methode, um in Sammlungen zu suchen, die in einer Beziehung zwischen über- und untergeordneten Elementen verknüpft sind.
Best Practices
- Speicher mit
toArrayExpression()verwalten:Seien Sie vorsichtig mittoArrayExpression()Unterabfragen, da die Materialisierung einer großen Anzahl von Dokumenten das Speicherlimit für Abfragen (128 MiB) überschreiten kann. Um dies zu vermeiden, verwenden Sieselect(...)in der Unterabfrage, um nur die erforderlichen Felder zurückzugeben, und wenden Siewhere(...)-Filter an, um die Anzahl der zurückgegebenen Dokumente zu begrenzen. Verwenden Sie gegebenenfallslimit(...), um die Anzahl der von der Unterabfrage zurückgegebenen Dokumente zu begrenzen. - Indexierung: Achten Sie darauf, dass die Felder, die in der
where(...)-Klausel einer Unterabfrage verwendet werden, indexiert sind. Leistungsstarke Joins beruhen darauf, dass Indexsuchen anstelle von vollständigen Tabellenscans ausgeführt werden können.
Weitere Best Practices für Abfragen finden Sie in unserem Leitfaden zur Abfrageoptimierung.
Beschränkungen
subcollection(...)-Bereich: Diesubcollection(...)Eingabephase wird nur in Unterabfragen unterstützt, da der Kontext eines übergeordneten Dokuments erforderlich ist, um die hierarchische Beziehung aufzulösen und den Join auszuführen.- Verschachtelungstiefe:Unterabfragen können bis zu 20 Ebenen tief verschachtelt werden.
- Speichernutzung:Das Limit von 128 MiB für materialisierte Daten gilt für die gesamte Abfrage, einschließlich aller verknüpften Dokumente.