總覽
Firestore Enterprise 版支援透過相關子查詢進行關聯式聯結。許多 NoSQL 資料庫通常需要將資料去正規化,或執行多個用戶端要求,但子查詢可讓您直接在伺服器上,合併及匯總相關集合或子集合的資料。
子查詢是運算式,會針對外部查詢處理的每份文件執行巢狀管道。這可啟用複雜的資料擷取模式,例如擷取文件及其相關子集合項目,或在不同的根集合中聯結邏輯上連結的資料。
概念
本節將介紹使用子查詢在管道作業中執行聯結的核心概念。
以運算式形式呈現的子查詢
子查詢不是頂層階段,而是運算式,可用於接受運算式的任何階段,例如 select(...)、add_fields(...)、where(...) 或 sort(...)。
Cloud Firestore 支援三種子查詢:
- 陣列子查詢:將子查詢的整個結果集具體化為文件陣列。
- 純量子查詢:評估為單一值,例如計數、平均值或相關文件中的特定欄位。
subcollection(...)子查詢:簡化一對多父項/子項關係的聯結。
範圍和變數
撰寫聯結時,巢狀子查詢通常需要參照「外部」文件 (父項) 中的欄位。如要橋接這些範圍,請使用 let(...) 階段 (在某些 SDK 中稱為 define(...)),在父項範圍中定義變數,然後使用 variable(...) 函式在子查詢中參照這些變數。
語法
以下各節將概述執行聯結的語法。
let(...) 階段
let(...) 階段 (在某些 SDK 中稱為 define(...)) 是非篩選階段,會明確將父項範圍的資料帶入具名變數,供後續巢狀範圍使用。
陣列子查詢
陣列子查詢是運算式子查詢的特例,可將子查詢的整個結果集具體化為陣列。如果子查詢傳回零個資料列,就會評估為空陣列。一律不會傳回 null 陣列。如果最終結果需要完整結果,例如具體化巢狀或相關集合時,這類查詢就很有用。
查詢可以在子查詢中篩選、排序及匯總,減少需要擷取及傳回的資料量,進而降低查詢成本。系統會遵守子查詢的順序,也就是說,子查詢中的 sort(...) 階段會控管最終陣列中的結果順序。
使用 toArrayExpression() SDK 包裝函式,將查詢轉換為陣列。
純量子查詢
純量子查詢通常用於 select(...) 或 where(...) 階段,做為允許篩選或產生子查詢結果的用途,而不會直接具體化完整查詢。
產生零結果的純量子查詢會評估為 null 本身,而評估為多個元素的子查詢則會導致執行階段錯誤。
如果純量子查詢為每個結果只產生一個欄位,該欄位會提升為子查詢的頂層結果。如果子查詢以 select(field("user_name")) 或 aggregate(countAll().as("total")) 結尾,且子查詢的結構定義只有單一欄位,最常會發生這種情況。否則,如果子查詢可以產生多個欄位,這些欄位會包裝在對應中。
使用 toScalarExpression() SDK 包裝函式,將查詢轉換為純量運算式。
subcollection(...) 子查詢
雖然是做為階段提供,但subcollection(...) 輸入階段可對 Cloud Firestore 的階層式資料模型執行聯結。在階層式模型中,查詢通常需要擷取文件,以及來自其子集合的資料。雖然您可以使用 collection_group(...) 輸入階段,然後對父項參照套用篩選器來達成此目的,但 subcollection(...) 提供的語法簡潔許多。
除了隱含的彙整條件外,這項作業的行為與陣列子查詢類似,即使巢狀集合不存在,如果沒有相符的文件,也會傳回空白結果。
這基本上是語法糖:系統會自動使用外部範圍中文件的 __name__ 做為聯結鍵,解析階層式關係。因此,這是跨從屬關係中連結的集合執行查閱作業的首選方式。
範例
範例資料
以下範例會載入一組測試資料,供後續所有範例使用。
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" });
在另一個集合中查閱文件
以下對 reviews 產品素材資源集合群組執行的查詢,會使用主鍵參照在 restaurant 產品素材資源集合群組中執行查閱。
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")));
回應
{
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" }
}
合併多個集合
以下查詢會從 restaurants 集合群組擷取所有披薩店,並使用陣列子查詢擷取相關聯的評論,直接嵌入回應中。
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")));
回應
{
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" }
]
}
匯總多個集合的資料
下列 restaurants 產品素材資源集合群組的查詢會使用相關子查詢,從 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")));
回應
{
name: "Golden Gate Pizza",
avg_rating: 4.5
},
{
name: "Venice Pizza",
avg_rating: 3.0
}
每個群組的前 N 個項目 (含限制的子查詢)
下列查詢會從 restaurants 集合群組擷取所有文件,並使用相關子查詢,擷取每間餐廳評分最高的前 2 則評論。
這樣可確保評論陣列不會過大,而達到查詢的記憶體限制。
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")));
回應
{
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" }
]
}
加入子集合
下列查詢會掃描 cities 集合,並使用 subcollection(...) 階段,隱含地聯結巢狀集合中的文件,找出每個城市中的餐廳數量。
Node.js
let results = await execute(db.pipeline()
.collection("cities")
.addFields(subcollection("restaurants")
.toArrayExpression()
.length()
.as("restaurant_count")));
回應
{
__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
}
表示多個聯結條件
下列查詢會掃描 restaurants 產品素材資源集合群組,並與 reviews 產品素材資源集合群組執行多欄位聯結,找出正在查看自家餐廳的擁有者。
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)));
回應
{
__name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
}
Anti-Join (NOT EXISTS)
下列查詢會掃描 restaurants 產品素材資源集合群組,找出所有尚未有評論的餐廳。
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)));
回應
{
__name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
}
子查詢做為聯結
下列查詢會將每個披薩店及其評論之間的關係扁平化。將子查詢放在 unnest(...) 階段中,伺服器會為每個相符的評論複製外部餐廳文件,產生扁平的聯結文件 (類似於 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")));
回應
{
__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" }
}
做為篩選器的不相關子查詢
reviews 集合上的下列查詢會使用不相關的子查詢,對自身執行篩選器,找出評分高於平均值的評論。
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");
回應
{
rating: 5,
reviewer_id "Alice"
},
{
rating: 5,
reviewer_id "Diana"
},
{
rating: 5,
reviewer_id "Hannah"
},
{
rating: 5,
reviewer_id "Julia"
}
最佳做法
- 使用
toArrayExpression()管理記憶體:請謹慎使用toArrayExpression()子查詢,因為實現大量文件可能會耗盡查詢記憶體限制 (128 MiB)。為減輕這項問題的影響,請在子查詢中使用select(...),只傳回必要的欄位,並套用where(...)篩選器,限制傳回的文件數量。如果合適,請考慮使用limit(...),限制子查詢傳回的文件數量。 - 建立索引:請確保子查詢的
where(...)子句中使用的欄位已建立索引。高效能的聯結取決於執行索引搜尋的能力,而非完整資料表掃描。
如需更多查詢最佳做法,請參閱查詢最佳化指南。
限制
subcollection(...)範圍:subcollection(...)輸入階段僅支援子查詢,因為需要父項文件的內容來解析階層式關係並執行聯結。- 巢狀結構深度:子查詢最多可巢狀結構化 20 層。
- 記憶體用量:實體化資料的 128 MiB 限制適用於整個查詢,包括所有已聯結的文件。