Übersicht
Die Firestore Enterprise-Edition unterstützt Joins im relationalen Stil 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 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. Dies ermöglicht komplexe Muster zum Abrufen von Daten, 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 Phase auf oberster 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.
Bereich 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 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 Daten zu reduzieren, die abgerufen und zurückgegeben werden müssen, 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.
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 hochgestuft und ist das Ergebnis der Unterabfrage auf oberster Ebene. 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 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 untergeordneten Sammlungen 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ägnantere 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.
Beispiele
Beispieldaten
Im Folgenden werden einige 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 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 Phase
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(...)-Phase 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 Filter mit einer nicht korrelierten Unterabfrage für sich selbst aus, um Rezensionen zu finden, die eine höhere Bewertung als der Durchschnitt haben.
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 erfordern, 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.