Übersicht
Die Firestore Enterprise-Edition unterstützt relationale Joins über 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 untergeordneten Sammlungen 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. Dadurch werden komplexe Muster zum Abrufen von Daten möglich, z. B. das Abrufen eines Dokuments zusammen mit den zugehörigen Elementen der untergeordneten Sammlung 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 Pipelinevorgängen vorgestellt.
Unterabfragen als Ausdrücke
Eine Unterabfrage ist keine Stage der obersten Ebene, sondern ein Ausdruck, der
in jeder Stage 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.
Bereich und Variablen
Beim Schreiben eines Joins müssen in der verschachtelten Unterabfrage häufig Felder aus dem „äußeren“ Dokument (dem übergeordneten Element) referenziert werden. Um diese Bereiche zu überbrücken, verwenden Sie die
let(...) Stage (in einigen
SDKs als define(...) bezeichnet), um Variablen im übergeordneten Bereich zu definieren, die dann in der
Unterabfrage mit der Funktion variable(...) referenziert werden können.
Syntax
In den folgenden Abschnitten erhalten Sie einen Überblick über die Syntax zum Ausführen von Joins.
Die Stage let(...)
Die let(...) Stage (in einigen
SDKs als define(...) bezeichnet) ist eine nicht filternde Stage, die Daten aus dem übergeordneten Bereich
explizit in eine benannte Variable einfügt, damit sie in nachfolgenden verschachtelten Bereichen verwendet werden können.
Array-Unterabfragen
Eine Array-Unterabfrage ist ein Sonderfall der Ausdruck-Unterabfrage, bei der das gesamte Ergebnis der Unterabfrage in ein 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(...)-Stage 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.
Skalare Unterabfragen
Skalare Unterabfragen werden häufig in einer select(...) oder
where(...) Stage 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 hochgestuft 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.
subcollection(...)-Unterabfragen
Die Eingabestage
subcollection(...) wird als Stage angeboten und ermöglicht
Joins für das hierarchische Datenmodell von Cloud Firestore's. In einem hierarchischen Modell müssen Abfragen häufig ein Dokument zusammen mit Daten aus den zugehörigen untergeordneten Sammlungen abrufen. Sie können dies mit einer
collection_group(...)-Eingabestage 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 diese Stage ähnlich wie eine Array-Unterabfrage. Sie gibt ein leeres Ergebnis zurück, wenn keine Dokumente gefunden werden, auch wenn die verschachtelte Sammlung nicht vorhanden ist.
Es handelt sich im Grunde um syntaktischen Zucker: Die Stage verwendet automatisch __name__ des Dokuments im äußeren Bereich als Join-Schlüssel, um die hierarchische Beziehung aufzulösen. Daher ist sie die bevorzugte Methode, um in Sammlungen zu suchen, die in einer Beziehung zwischen über- und untergeordneten Elementen verknüpft sind.
Beispiele
Beispieldaten
Im Folgenden werden Testdaten geladen, die in allen folgenden Beispielen verwendet werden.
Node.js
// Load set of cities.
const cities = collection(db, "cities");
await setDoc(doc(cities, "SF"), {
name: "San Francisco",
state: "CA",
country: "USA",
});
await setDoc(doc(cities, "LA"), {
name: "Los Angeles",
state: "CA",
country: "USA"
});
await setDoc(doc(cities, "DC"), {
name: "Washington, D.C.",
state: null,
country: "USA"
});
await setDoc(doc(cities, "TOK"), {
name: "Tokyo",
state: null,
country: "Japan"
});
// Load restaurants in various cities.
const sfRestaurants = collection(db, "cities", "SF", "restaurants");
const laRestaurants = collection(db, "cities", "LA", "restaurants");
const dcRestaurants = collection(db, "cities", "DC", "restaurants");
const rest1 = await addDoc(sfRestaurants, {
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi"
});
const rest2 = await addDoc(sfRestaurants, {
name: "Bay Area Burger",
type: "burger",
owner_id: "Sarah Jenkins"
});
const rest3 = await addDoc(sfRestaurants, {
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
});
const rest4 = await addDoc(laRestaurants, {
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
});
const rest5 = await addDoc(laRestaurants, {
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano"
});
const rest6 = await addDoc(dcRestaurants, {
name: "Capitol Tacos",
type: "mexican",
owner_id: "Maria Garcia"
});
const rest7 = await addDoc(dcRestaurants, {
name: "Georgetown Coffee",
type: "cafe",
owner_id: "David Kim"
});
// Load collection of reviews.
const reviews = collection(db, "reviews");
await addDoc(reviews, { restaurant: rest1, rating: 5, reviewer_id "Alice" });
await addDoc(reviews, { restaurant: rest1, rating: 4, reviewer_id "Bob" });
await addDoc(reviews, { restaurant: rest2, rating: 4, reviewer_id "Charlie" });
await addDoc(reviews, { restaurant: rest3, rating: 5, reviewer_id "Diana" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Edward" });
await addDoc(reviews, { restaurant: rest3, rating: 4, reviewer_id "Fiona" });
// rest4 has 0 reviews
await addDoc(reviews, { restaurant: rest5, rating: 3, reviewer_id "George" });
await addDoc(reviews, { restaurant: rest6, rating: 5, reviewer_id "Hannah" });
await addDoc(reviews, { restaurant: rest6, rating: 4, reviewer_id "Ian" });
await addDoc(reviews, { restaurant: rest7, rating: 5, reviewer_id "Julia" });
Dokument in einer anderen Sammlung suchen
Die folgende Abfrage für die Sammlungsgruppe reviews führt eine Suche in der Sammlungsgruppe restaurant mit einer Primärschlüsselreferenz durch.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("reviews")
.define(field("restaurant").as("restaurant_name"))
.addFields(db.pipeline()
.collectionGroup("restaurant")
.where(field("__name__").equal(variable("restaurant_name")))
.select("name", "type")
.toScalarExpression()
.as("restaurant")));
Antwort
{
rating: 5,
reviewer_id "Alice",
restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
rating: 4,
reviewer_id "Bob",
restaurant: { name: "Golden Gate Pizza", type: "pizza" }
},
{
rating: 4,
reviewer_id "Charlie",
restaurant: { name: "Bay Area Burger", type: "burger" }
},
{
rating: 5,
reviewer_id "Diana",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 4,
reviewer_id "Edward",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 4,
reviewer_id "Fiona",
restaurant: { name: "Sunset Taco", type: "mexican" }
},
{
rating: 3,
reviewer_id "George",
restaurant: { name: "Venice Pizza", type: "pizza" }
},
{
rating: 5,
reviewer_id "Hannah",
restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
rating: 4,
reviewer_id "Ian",
restaurant: { name: "Capitol Tacos", type: "mexican" }
},
{
rating: 5,
reviewer_id "Julia",
restaurant: { name: "Georgetown Coffee", type: "cafe" }
}
Mehrere Sammlungen kombinieren
Die folgende Abfrage ruft alle Pizzerien aus der Sammlungsgruppe restaurants ab und verwendet eine Array-Unterabfrage, um die zugehörigen Rezensionen abzurufen und direkt in die Antwort einzubetten.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.select("rating", "reviewer_id")
.toArrayExpression()
.as("reviews")));
Antwort
{
name: "Golden Gate Pizza",
reviews: [
{ rating: 5, reviewer_id "Alice" },
{ rating: 4, reviewer_id "Bob" }
]
},
{
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano",
reviews: [
{ rating: 3, reviewer_id "George" }
]
}
Über mehrere Sammlungen hinweg aggregieren
Die folgende Abfrage für die Sammlungsgruppe restaurants verwendet eine korrelierte Unterabfrage, um die durchschnittliche Bewertung für jedes Restaurant aus der Sammlungsgruppe reviews abzurufen.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.aggregate(average("rating").as("avg_rating"))
.toScalarExpression()
.as("avg_rating")));
Antwort
{
name: "Golden Gate Pizza",
avg_rating: 4.5
},
{
name: "Venice Pizza",
avg_rating: 3.0
}
Top-N pro Gruppe (Unterabfrage mit Limit)
Die folgende Abfrage ruft alle Dokumente aus der Sammlungsgruppe restaurants ab und verwendet eine korrelierte Unterabfrage, um die beiden am besten bewerteten Rezensionen für jedes Restaurant abzurufen.
So wird verhindert, dass das Array von Rezensionen zu groß wird und das Speicherlimit der Abfrage erreicht.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.define(field("__name__").as("restaurant_name"))
.select(
field("name"),
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.sort(field("rating").descending())
.limit(2)
.select("rating", "reviewer_id")
.toArrayExpression()
.as("top_reviews")));
Antwort
{
name: "Golden Gate Pizza",
top_reviews: [
{ rating: 5, reviewer_id "Alice" },
{ rating: 4, reviewer_id "Bob" }
]
},
{
name: "Bay Area Burger",
top_reviews: [
{ rating: 4, reviewer_id "Charlie" }
]
},
{
name: "Sunset Taco",
top_reviews: [
{ rating: 5, reviewer_id "Diana" },
{ rating: 4, reviewer_id "Edward" }
]
},
{
name: "Hollywood Sushi",
top_reviews: []
},
{
name: "Venice Pizza",
top_reviews: [
{ rating: 3, reviewer_id "George" }
]
},
{
name: "Capitol Tacos",
top_reviews: [
{ rating: 5, reviewer_id "Hannah" },
{ rating: 4, reviewer_id "Ian" }
]
},
{
name: "Georgetown Coffee",
top_reviews: [
{ rating: 5, reviewer_id "Julia" }
]
}
Untergeordnete Sammlungen verknüpfen
Die folgende Abfrage scannt die Sammlung cities und verwendet die Stage
subcollection(...), um implizit Joins
für Dokumente aus einer verschachtelten Sammlung auszuführen und die Anzahl der Restaurants pro
Stadt zu ermitteln.
Node.js
let results = await execute(db.pipeline()
.collection("cities")
.addFields(subcollection("restaurants")
.toArrayExpression()
.length()
.as("restaurant_count")));
Antwort
{
__name__: cities/SF,
name: "San Francisco",
state: "CA",
country: "USA",
restaurant_count: 3
},
{
__name__: cities/LA,
name: "Los Angeles",
state: "CA",
country: "USA",
restaurant_count: 2
},
{
__name__: cities/DC,
name: "Washington, D.C.",
state: null,
country: "USA",
restaurant_count: 2
},
{
__name__: cities/TOK,
name: "Tokyo",
state: null,
country: "Japan",
restaurant_count: 0
}
Mehrere Join-Bedingungen ausdrücken
Die folgende Abfrage scannt die Sammlungsgruppe restaurants und führt einen Join mit mehreren Feldern mit der Sammlungsgruppe reviews aus, um Inhaber zu finden, die ihre eigenen Restaurants bewerten.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.define(field("owner_id"), field("__name__"))
.where(db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("__name__")))
.where(field("author").equal(variable("owner_id")))
.aggregate(count().as("c"))
.toScalarExpression()
.greaterThan(0)));
Antwort
{
__name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
}
Anti-Join (NOT EXISTS)
Die folgende Abfrage scannt die Sammlungsgruppe restaurants und findet alle Restaurants, für die noch keine Rezensionen vorhanden sind.
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.define(field("__name__").as("restaurant_name"))
.where(db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.aggregate(count().as("review_count"))
.toScalarExpression()
.equal(0)));
Antwort
{
__name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
}
Unterabfrage als Join
Die folgende Abfrage vereinfacht die Beziehung zwischen jeder Pizzeria und ihren Rezensionen. Wenn Sie die Unterabfrage in eine
unnest(...)-Stage einfügen, dupliziert der Server das äußere
Restaurantdokument für jede übereinstimmende Rezension und erzeugt so vereinfachte, verknüpfte Dokumente
(ähnlich einem SQL-INNER JOIN).
Node.js
let results = await execute(db.pipeline()
.collectionGroup("restaurants")
.where(field("type").equal("pizza"))
.define(field("__name__").as("restaurant_name"))
.unnest(
db.pipeline()
.collectionGroup("reviews")
.where(field("restaurant").equal(variable("restaurant_name")))
.select("rating", "reviewer_id")
.toArrayExpression()
.as("review")));
Antwort
{
__name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi"
review: { rating: 5, reviewer_id "Alice" }
},
{
__name__: "cities/SF/restaurants/xU4pu8nFpnJDPZOwcSPP",
name: "Golden Gate Pizza",
type: "pizza",
owner_id: "Mario Rossi",
review: { rating: 4, reviewer_id "Bob" }
},
{
__name__: "cities/LA/restaurants/6CYntvNgbYzgaW652Gq1",
name: "Venice Pizza",
type: "pizza",
owner_id: "Luigi Romano",
review: { rating: 3, reviewer_id "George" }
}
Nicht korrelierte Unterabfrage als Filter
Die folgende Abfrage für die Sammlung reviews führt Filterungen mit einer nicht korrelierten Unterabfrage für sich selbst durch, um Rezensionen zu finden, die über der durchschnittlichen Bewertung liegen.
Node.js
let results = await execute(db.pipeline()
.collection("reviews")
// Average review rating is 4.3
.where(field("rating").greaterThan(db.pipeline()
.collection("reviews")
.aggregate(average("rating").as("avg"))
.toScalarExpression())))
.select("rating", "reviewer_id");
Antwort
{
rating: 5,
reviewer_id "Alice"
},
{
rating: 5,
reviewer_id "Diana"
},
{
rating: 5,
reviewer_id "Hannah"
},
{
rating: 5,
reviewer_id "Julia"
}
Best Practices
- Speicher mit
toArrayExpression()verwalten:Seien Sie vorsichtig mittoArrayExpression()Unterabfragen, da die Materialisierung einer großen Anzahl von Dokumenten das Speicherlimit der Abfrage (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 sind darauf angewiesen, dass Indexsuchen anstelle von vollständigen Tabellenscans durchgeführt werden können.
Weitere Best Practices für Abfragen finden Sie in unserem Leitfaden zur Abfrageoptimierung.
Beschränkungen
subcollection(...)-Bereich: Diesubcollection(...)Eingabestage wird nur in Unterabfragen unterstützt, da sie den Kontext eines übergeordneten Dokuments benötigt, 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.