ภาพรวม
รุ่น Firestore Enterprise รองรับการรวมสไตล์เชิงสัมพันธ์ผ่านการสืบค้นย่อยที่สัมพันธ์กัน ไม่เหมือนกับฐานข้อมูล NoSQL หลายรายการที่มักต้อง ยกเลิกการทำให้ข้อมูลเป็นปกติหรือทำการขอฝั่งไคลเอ็นต์หลายรายการ คิวรีย่อยช่วยให้คุณ รวมและรวบรวมข้อมูลจากคอลเล็กชันหรือคอลเล็กชันย่อยที่เกี่ยวข้อง ได้โดยตรงบนเซิร์ฟเวอร์
การค้นหาย่อยคือนิพจน์ที่เรียกใช้ไปป์ไลน์ที่ซ้อนกันสำหรับทุกเอกสาร ที่ประมวลผลโดยการค้นหาภายนอก ซึ่งช่วยให้ใช้รูปแบบการดึงข้อมูลที่ซับซ้อนได้ เช่น การดึงข้อมูลเอกสารพร้อมกับรายการในคอลเล็กชันย่อยที่เกี่ยวข้อง หรือ การรวมข้อมูลที่ลิงก์กันอย่างมีตรรกะในคอลเล็กชันระดับรูทที่แตกต่างกัน
แนวคิด
ส่วนนี้จะแนะนําแนวคิดหลักเบื้องหลังการใช้คําสั่งย่อยเพื่อ ทําการรวมในการดําเนินการไปป์ไลน์
Subquery เป็นนิพจน์
Subquery ไม่ใช่ขั้นตอนระดับบนสุด แต่เป็นนิพจน์ที่
ใช้ได้ในทุกขั้นตอนที่ยอมรับนิพจน์ เช่น
select(...)
add_fields(...)
where(...) หรือ sort(...)
Cloud Firestore รองรับคําค้นหาย่อย 3 ประเภท ได้แก่
- การสืบค้นย่อยของอาร์เรย์: สร้างชุดผลลัพธ์ทั้งหมดของการสืบค้นย่อยเป็นอาร์เรย์ของเอกสาร
- การสืบค้นย่อยแบบสเกลาร์: ประเมินเป็นค่าเดียว เช่น จำนวน ค่าเฉลี่ย หรือฟิลด์ที่เฉพาะเจาะจงจากเอกสารที่เกี่ยวข้อง
subcollection(...)Subquery: การรวมที่ง่ายขึ้นสำหรับความสัมพันธ์แบบหนึ่งต่อหลายรายการ ระหว่างรายการหลักกับรายการย่อย
ขอบเขตและตัวแปร
เมื่อเขียนการรวม คุณมักจะต้องอ้างอิงฟิลด์จากเอกสาร "ภายนอก" (ระดับบนสุด) ในการค้นหาย่อยที่ซ้อนกัน เพื่อเชื่อมขอบเขตเหล่านี้ ให้ใช้ขั้นตอน let(...) (เรียกว่า define(...) ใน SDK บางรายการ) เพื่อกำหนดตัวแปรในขอบเขตระดับบนสุด ซึ่งจะอ้างอิงได้ในการค้นหาย่อยโดยใช้ฟังก์ชัน variable(...)
ไวยากรณ์
ส่วนต่อไปนี้จะแสดงภาพรวมของไวยากรณ์สำหรับการรวม
let(...) ขั้นตอน
ขั้นตอนlet(...) (เรียกว่า define(...) ใน SDK บางรายการ) เป็นขั้นตอนที่ไม่ใช่การกรองซึ่งนำข้อมูลจากขอบเขตระดับบนมาไว้ในตัวแปรที่มีชื่ออย่างชัดเจนเพื่อใช้ในขอบเขตที่ซ้อนกันในภายหลัง
การสืบค้นย่อยของอาร์เรย์
การค้นหาย่อยของอาร์เรย์เป็นกรณีพิเศษของการค้นหาย่อยของนิพจน์ที่สร้างชุดผลลัพธ์ทั้งหมดของการค้นหาย่อยเป็นอาร์เรย์ หากการค้นหาย่อยแสดงผลเป็น 0 แถว การค้นหาย่อยจะประเมินเป็นอาร์เรย์ว่าง การค้นหาย่อยจะไม่แสดงผลอาร์เรย์ null การค้นหาดังกล่าวมีประโยชน์เมื่อต้องใช้ผลลัพธ์ทั้งหมดในผลลัพธ์สุดท้าย เช่น เมื่อสร้างคอลเล็กชันที่ซ้อนกันหรือสัมพันธ์กัน
การค้นหาสามารถกรอง จัดเรียง และรวบรวมในคำสั่งย่อยเพื่อลดปริมาณ
ข้อมูลที่ต้องดึงและส่งคืนเพื่อช่วยลดค่าใช้จ่ายของการค้นหาได้
ระบบจะพิจารณาลำดับของคำสั่งย่อย ซึ่งหมายความว่าขั้นตอน sort(...)
ในคำสั่งย่อยจะควบคุมลำดับของผลลัพธ์ในอาร์เรย์สุดท้าย
ใช้ Wrapper ของ toArrayExpression() SDK เพื่อแปลงการค้นหาเป็นอาร์เรย์
การสืบค้นย่อยแบบสเกลาร์
มักใช้คิวรีย่อยแบบสเกลาร์ในขั้นตอน select(...) หรือ
where(...) เพื่ออนุญาตการกรองหรือแสดงผล
ผลลัพธ์ของคิวรีย่อยโดยไม่ต้องสร้างคิวรีแบบเต็มโดยตรง
Subquery แบบสเกลาร์ที่ให้ผลลัพธ์เป็น 0 จะประเมินเป็น null เอง
ในขณะที่ Subquery ที่ประเมินเป็นองค์ประกอบหลายรายการจะทำให้เกิดข้อผิดพลาดรันไทม์
เมื่อคิวรีย่อยแบบสเกลาร์สร้างฟิลด์เดียวต่อผลลัพธ์ ระบบจะยกระดับฟิลด์นั้นให้เป็นผลลัพธ์ระดับบนสุดสำหรับคิวรีย่อย โดยมักจะเห็นได้เมื่อคำสั่งย่อยลงท้ายด้วย select(field("user_name")) หรือ aggregate(countAll().as("total")) ซึ่งสคีมาของคำสั่งย่อยเป็นเพียงฟิลด์เดียว มิฉะนั้น เมื่อคิวรีย่อยสร้างฟิลด์ได้หลายรายการ ระบบจะ
ห่อฟิลด์เหล่านั้นไว้ในแมป
ใช้ toScalarExpression() SDK Wrapper เพื่อแปลงการค้นหาเป็นนิพจน์สเกลาร์
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
}
Top-N ต่อกลุ่ม (คำสั่งย่อยที่มี LIMIT)
การค้นหาต่อไปนี้จะดึงข้อมูลเอกสารทั้งหมดจากคอลเล็กชัน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กลุ่มคอลเล็กชันและทำการ
รวมหลายฟิลด์กับ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"
}
การป้องกันการเข้าร่วม (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"
}
Subquery as Join
คำค้นหาต่อไปนี้จะทำให้ความสัมพันธ์ระหว่างร้านพิซซ่าแต่ละร้านกับรีวิว
แบนราบ การวางคําค้นหาย่อยไว้ภายในขั้นตอน unnest(...) จะทําให้เซิร์ฟเวอร์ทําซ้ำเอกสารร้านอาหารภายนอกสําหรับรีวิวที่ตรงกันแต่ละรายการ ซึ่งจะสร้างเอกสารแบบแบนที่รวมกัน (คล้ายกับ INNER JOIN ของ SQL)
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 สำหรับข้อมูลที่สร้างขึ้นจะมีผลกับทั้งคำค้นหา รวมถึงเอกสารที่เข้าร่วมทั้งหมด