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