Przegląd
Wersja Firestore Enterprise obsługuje złączenia w stylu relacyjnym za pomocą powiązanych podzapytań. W przeciwieństwie do wielu baz danych NoSQL, które często wymagają denormalizacji danych lub wykonywania wielu żądań po stronie klienta, zapytania podrzędne umożliwiają łączenie i agregowanie danych z powiązanych kolekcji lub podkolekcji bezpośrednio na serwerze.
Podzapytania to wyrażenia, które wykonują zagnieżdżony potok dla każdego dokumentu przetwarzanego przez zapytanie zewnętrzne. Umożliwia to złożone wzorce pobierania danych, takie jak pobieranie dokumentu wraz z powiązanymi elementami podkolekcji lub łączenie logicznie powiązanych danych z różnych kolekcji głównych.
Pojęcia
W tej sekcji przedstawiamy podstawowe koncepcje związane z używaniem podzapytań do wykonywania złączeń w operacjach potoku.
Podzapytania jako wyrażenia
Podzapytanie nie jest etapem najwyższego poziomu, ale wyrażeniem, którego można używać na każdym etapie akceptującym wyrażenia, np. select(...), add_fields(...), where(...) lub sort(...).
Cloud Firestore obsługuje 3 typy zapytań podrzędnych:
- Podzapytania tablicowe: materializują cały zbiór wyników podzapytania jako tablicę dokumentów.
- Podzapytania skalarne: zwracają pojedynczą wartość, np. liczbę, średnią lub konkretne pole z powiązanego dokumentu.
subcollection(...)Zapytania podrzędne: uproszczone złączenia w relacji nadrzędny-podrzędny typu jeden do wielu.
Zakres i zmienne
Podczas pisania złączenia zagnieżdżone podzapytanie często musi odwoływać się do pól z dokumentu „zewnętrznego” (nadrzędnego). Aby połączyć te zakresy, użyj etapu let(...) (w niektórych pakietach SDK nazywanego define(...)), aby zdefiniować zmienne w zakresie nadrzędnym, do których można się odwoływać w podzapytaniu za pomocą funkcji variable(...).
Składnia
W sekcjach poniżej znajdziesz omówienie składni wykonywania złączeń.
Etap let(...)
Etap let(...) (w niektórych pakietach SDK określany jako define(...)) to etap bez filtrowania, który jawnie przenosi dane z zakresu nadrzędnego do nazwanej zmiennej, aby można było ich używać w kolejnych zakresach zagnieżdżonych.
Podzapytania tablicowe
Podzapytanie tablicowe to szczególny przypadek podzapytania wyrażenia, które materializuje cały zbiór wyników podzapytania w tablicy. Jeśli podzapytanie zwraca zero wierszy, jest ono oceniane jako pusta tablica. Nigdy nie zwraca tablicy null. Takie zapytania są przydatne, gdy w wyniku końcowym wymagane są pełne wyniki, np. podczas materializowania zagnieżdżonej lub powiązanej kolekcji.
Zapytania mogą filtrować, sortować i agregować dane w podzapytaniu, aby zmniejszyć ilość danych, które trzeba pobrać i zwrócić, co pomaga obniżyć koszt zapytania. Kolejność podzapytania jest zachowywana, co oznacza, że etap sort(...) w podzapytaniu kontroluje kolejność wyników w tablicy końcowej.
Użyj otoki pakietu SDK toArrayExpression(), aby przekształcić zapytanie w tablicę.
Podzapytania skalarne
Podzapytania skalarne są często używane na etapie select(...) lub where(...), ponieważ umożliwiają filtrowanie lub uzyskiwanie wyniku podzapytania bez bezpośredniego materializowania pełnego zapytania.
Podzapytanie skalarne, które nie zwraca żadnych wyników, będzie miało wartość null, natomiast podzapytanie, które zwraca wiele elementów, spowoduje błąd w czasie działania.
Gdy podzapytanie skalarne zwraca tylko jedno pole na wynik, pole jest podnoszone do poziomu wyniku najwyższego poziomu dla podzapytania. Najczęściej występuje to, gdy podzapytanie kończy się na select(field("user_name")) lub aggregate(countAll().as("total")), a schemat podzapytania zawiera tylko jedno pole. W przeciwnym razie, gdy podzapytanie może zwrócić wiele pól, są one zawijane w mapę.
Użyj toScalarExpression()otoczki pakietu SDK, aby przekształcić zapytanie w wyrażenie skalarne.
subcollection(...) Podzapytania
Etap wejściowy subcollection(...) umożliwia wykonywanie złączeń w hierarchicznym modelu danych Cloud Firestore. W modelu hierarchicznym zapytania często muszą pobierać dokument wraz z danymi z jego podzbiorów. Możesz to osiągnąć, używając etapu wejściowego collection_group(...), a następnie filtra odwołania do elementu nadrzędnego, ale subcollection(...) zapewnia znacznie bardziej zwięzłą składnię.
Poza niejawnym warunkiem łączenia działa podobnie do podzapytania array, zwracając pusty wynik, jeśli nie pasuje żaden dokument, nawet jeśli zagnieżdżona kolekcja nie istnieje.
Jest to w zasadzie syntaktyczny cukier: automatycznie używa __name__ dokumentu w zakresie zewnętrznym jako klucza złączenia do rozwiązania relacji hierarchicznej. Dlatego jest to preferowany sposób wyszukiwania w kolekcjach połączonych relacją nadrzędny-podrzędny.
Przykłady
Przykładowe dane
Poniższy kod wczytuje zestaw danych testowych, który będzie używany we wszystkich kolejnych przykładach.
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" });
Wyszukiwanie dokumentu w innej kolekcji
Poniższe zapytanie w grupie kolekcji reviews wykonuje wyszukiwanie w grupie kolekcji restaurant za pomocą odwołania do klucza podstawowego.
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")));
Odpowiedź
{
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" }
}
Łączenie wielu kolekcji
To zapytanie pobiera wszystkie pizzerie z grupy kolekcji restaurants i używa podzapytania tablicowego, aby pobrać powiązane z nimi opinie i osadzić je bezpośrednio w odpowiedzi.
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")));
Odpowiedź
{
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" }
]
}
Agregowanie w wielu kolekcjach
Poniższe zapytanie dotyczące grupy kolekcji restaurants używa powiązanego podzapytania, aby uzyskać średnią ocenę każdej restauracji z grupy kolekcji reviews.
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")));
Odpowiedź
{
name: "Golden Gate Pizza",
avg_rating: 4.5
},
{
name: "Venice Pizza",
avg_rating: 3.0
}
N najlepszych wyników w grupie (podzapytanie z limitem)
Poniższe zapytanie pobiera wszystkie dokumenty z restaurants kolekcji
group i używa powiązanego podzapytania, aby pobrać 2 najwyżej ocenione opinie
o każdej restauracji.
Dzięki temu tablica opinii nie będzie zbyt duża i nie przekroczy limitu pamięci zapytania.
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")));
Odpowiedź
{
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" }
]
}
Dołączanie do podkolekcji
Poniższe zapytanie skanuje kolekcję cities i używa etapu subcollection(...), aby niejawnie połączyć dokumenty z zagnieżdżonej kolekcji i określić liczbę restauracji w poszczególnych miastach.
Node.js
let results = await execute(db.pipeline()
.collection("cities")
.addFields(subcollection("restaurants")
.toArrayExpression()
.length()
.as("restaurant_count")));
Odpowiedź
{
__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
}
Wyrażanie wielu warunków łączenia
To zapytanie skanuje grupę kolekcji restaurants i wykonuje złączenie wielu pól z grupą kolekcji reviews, aby znaleźć właścicieli, którzy sprawdzają własne restauracje.
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)));
Odpowiedź
{
__name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
}
Anti-Join (NOT EXISTS)
Poniższe zapytanie skanuje grupę kolekcji restaurants i znajduje wszystkie restauracje, które nie mają jeszcze opinii.
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)));
Odpowiedź
{
__name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
}
Podzapytanie jako złączenie
Poniższe zapytanie spłaszcza relację między każdą pizzerią a jej opiniami. Umieszczając podzapytanie w etapie unnest(...), serwer duplikuje zewnętrzny dokument restauracji dla każdej pasującej opinii, tworząc płaskie, połączone dokumenty (podobne do 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")));
Odpowiedź
{
__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" }
}
Podzapytanie nieskorelowane jako filtr
To zapytanie w kolekcji reviews filtruje dane za pomocą nieskorelowanego zapytania podrzędnego, aby znaleźć opinie o ocenie wyższej niż średnia.
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");
Odpowiedź
{
rating: 5,
reviewer_id "Alice"
},
{
rating: 5,
reviewer_id "Diana"
},
{
rating: 5,
reviewer_id "Hannah"
},
{
rating: 5,
reviewer_id "Julia"
}
Sprawdzone metody
- Zarządzanie pamięcią za pomocą
toArrayExpression(): zachowaj ostrożność w przypadku podzapytańtoArrayExpression(), ponieważ materializowanie dużej liczby dokumentów może wyczerpać limit pamięci zapytania (128 MiB). Aby temu zapobiec, użyj w podzapytaniu funkcjiselect(...), aby zwracać tylko niezbędne pola, i zastosuj filtrywhere(...), aby ograniczyć liczbę zwracanych dokumentów. W razie potrzeby użyjlimit(...), aby ograniczyć liczbę dokumentów zwracanych przez podzapytanie. - Indeksowanie: sprawdź, czy pola używane w klauzuli
where(...)podzapytania są indeksowane. Wydajne złączenia opierają się na możliwości wyszukiwania indeksów, a nie na pełnym skanowaniu tabeli.
Więcej sprawdzonych metod dotyczących zapytań znajdziesz w naszym przewodniku po optymalizacji zapytań.
Ograniczenia
subcollection(...)Zakres: etap wejściowysubcollection(...)jest obsługiwany tylko w zapytaniach podrzędnych, ponieważ wymaga kontekstu dokumentu nadrzędnego do rozwiązania relacji hierarchicznej i wykonania złączenia.- Głębokość zagnieżdżenia: zapytania podrzędne mogą być zagnieżdżone do 20 poziomów.
- Wykorzystanie pamięci: limit 128 MiB na zmaterializowane dane obowiązuje w przypadku całego zapytania, w tym wszystkich połączonych dokumentów.