Tổng quan
Phiên bản Enterprise của Firestore hỗ trợ các phép kết hợp theo kiểu quan hệ thông qua truy vấn con tương quan. Không giống như nhiều cơ sở dữ liệu NoSQL thường yêu cầu huỷ chuẩn hoá dữ liệu hoặc thực hiện nhiều yêu cầu phía máy khách, truy vấn con cho phép bạn kết hợp và tổng hợp dữ liệu từ các bộ sưu tập hoặc bộ sưu tập con có liên quan trực tiếp trên máy chủ.
Truy vấn con là các biểu thức thực thi một quy trình lồng nhau cho mọi tài liệu được xử lý bằng truy vấn bên ngoài. Điều này cho phép các mẫu truy xuất dữ liệu phức tạp, chẳng hạn như tìm nạp một tài liệu cùng với các mục trong bộ sưu tập con có liên quan hoặc kết hợp dữ liệu được liên kết một cách logic trên các bộ sưu tập gốc riêng biệt.
Khái niệm
Phần này giới thiệu các khái niệm cốt lõi đằng sau việc sử dụng truy vấn con để thực hiện các phép kết hợp trong các thao tác của Quy trình.
Truy vấn con dưới dạng biểu thức
Truy vấn con không phải là giai đoạn cấp cao nhất; thay vào đó, đây là một biểu thức có
thể được sử dụng trong bất kỳ giai đoạn nào chấp nhận biểu thức, chẳng hạn như
select(...),
add_fields(...),
where(...), hoặc sort(...).
Cloud Firestore hỗ trợ 3 loại truy vấn con:
- Truy vấn con mảng: Hiện thực hoá toàn bộ tập kết quả của truy vấn con dưới dạng một mảng tài liệu.
- Truy vấn con vô hướng: Đánh giá thành một giá trị duy nhất, chẳng hạn như số lượng, giá trị trung bình hoặc một trường cụ thể từ một tài liệu có liên quan.
- Truy vấn con
subcollection(...): các phép kết hợp đơn giản hoá cho mối quan hệ một-nhiều giữa cha-con.
Phạm vi và biến
Khi viết một phép kết hợp, truy vấn con lồng nhau thường cần tham chiếu đến các trường từ tài liệu "bên ngoài" (tài liệu mẹ). Để kết nối các phạm vi này, bạn sử dụng giai đoạn
let(...) (được gọi là define(...) trong một số
SDK) để xác định các biến trong phạm vi mẹ mà sau đó có thể được tham chiếu trong
truy vấn con bằng hàm variable(...).
Cú pháp
Các phần sau đây cung cấp thông tin tổng quan về cú pháp để thực hiện các phép kết hợp.
Giai đoạn let(...)
Giai đoạn let(...) (được gọi là define(...) trong một số
SDK) là một giai đoạn không lọc, giúp đưa dữ liệu một cách rõ ràng từ phạm vi mẹ
vào một biến có tên để sử dụng trong các phạm vi lồng nhau tiếp theo.
Truy vấn con mảng
Truy vấn con mảng là một trường hợp đặc biệt của truy vấn con biểu thức, giúp hiện thực hoá toàn bộ tập kết quả của truy vấn con thành một mảng. Nếu truy vấn con trả về 0 hàng, thì truy vấn con đó sẽ đánh giá thành một mảng trống. Truy vấn con này không bao giờ trả về một mảng null. Các truy vấn như vậy rất hữu ích khi bạn cần kết quả đầy đủ trong kết quả cuối cùng, chẳng hạn như khi hiện thực hoá một bộ sưu tập lồng nhau hoặc tương quan.
Truy vấn có thể lọc, sắp xếp và tổng hợp trong truy vấn con để giảm lượng dữ liệu cần tìm nạp và trả về, giúp giảm chi phí truy vấn. Thứ tự của truy vấn con được tôn trọng, nghĩa là giai đoạn sort(...) trong truy vấn con sẽ kiểm soát thứ tự của kết quả trong mảng cuối cùng.
Sử dụng trình bao bọc SDK toArrayExpression() để chuyển đổi một truy vấn thành một mảng.
Truy vấn con vô hướng
Truy vấn con vô hướng thường được sử dụng trong giai đoạn select(...) hoặc
where(...) vì cho phép lọc hoặc tạo kết quả của
một truy vấn con mà không hiện thực hoá trực tiếp toàn bộ truy vấn.
Một truy vấn con vô hướng tạo ra 0 kết quả sẽ tự đánh giá thành null, trong khi một truy vấn con đánh giá thành nhiều phần tử sẽ dẫn đến lỗi thời gian chạy.
Khi một truy vấn con vô hướng chỉ tạo ra một trường duy nhất cho mỗi kết quả, trường đó sẽ được nâng cao để trở thành kết quả cấp cao nhất cho truy vấn con. Điều này thường thấy nhất
khi truy vấn con kết thúc bằng select(field("user_name")) hoặc
aggregate(countAll().as("total")) trong đó lược đồ của truy vấn con chỉ là một
trường duy nhất. Nếu không, khi một truy vấn con có thể tạo ra nhiều trường, các trường đó sẽ được gói trong một bản đồ.
Sử dụng trình bao bọc SDK toScalarExpression() để chuyển đổi một truy vấn thành một biểu thức vô hướng.
Truy vấn con subcollection(...)
Mặc dù được cung cấp dưới dạng một giai đoạn, nhưng giai đoạn đầu vào
subcollection(...) cho phép
thực hiện các phép kết hợp trên mô hình dữ liệu phân cấp của Cloud Firestore's. Trong mô hình phân cấp, truy vấn thường cần truy xuất một tài liệu cùng với dữ liệu từ các bộ sưu tập con của tài liệu đó. Mặc dù bạn có thể thực hiện việc này bằng cách sử dụng giai đoạn đầu vào a
collection_group(...) rồi lọc theo tham chiếu mẹ, subcollection(...) cung cấp một cú pháp ngắn gọn hơn nhiều.
Ngoài điều kiện kết hợp ngầm ẩn, điều này hoạt động tương tự như một truy vấn con mảng, trả về kết quả trống nếu không có tài liệu nào khớp, ngay cả khi bộ sưu tập lồng nhau không tồn tại.
Về cơ bản, đây là cú pháp đường: nó tự động sử dụng __name__ của
tài liệu trong phạm vi bên ngoài làm khoá kết hợp để giải quyết mối quan hệ phân cấp. Điều này khiến nó trở thành cách ưu tiên để thực hiện các lượt tra cứu trên các bộ sưu tập được liên kết trong mối quan hệ cha-con.
Ví dụ
Dữ liệu mẫu
Phần sau đây tải một tập hợp dữ liệu kiểm thử để sử dụng trong tất cả các ví dụ sau.
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" });
Tra cứu một tài liệu trong một bộ sưu tập khác
Truy vấn sau đây trên nhóm bộ sưu tập reviews thực hiện tra cứu vào nhóm bộ sưu tập restaurant bằng cách sử dụng tham chiếu khoá chính.
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")));
Đáp
{
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" }
}
Kết hợp nhiều bộ sưu tập
Truy vấn sau đây tìm nạp tất cả các địa điểm bán bánh pizza từ nhóm bộ sưu tập restaurants và sử dụng một truy vấn con mảng để tìm nạp và nhúng các bài đánh giá được liên kết trực tiếp vào phản hồi.
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")));
Đáp
{
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" }
]
}
Tổng hợp trên nhiều bộ sưu tập
Truy vấn sau đây trên nhóm bộ sưu tập restaurants sử dụng một truy vấn con tương quan để lấy điểm xếp hạng trung bình cho từng nhà hàng từ nhóm bộ sưu tập 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")));
Đáp
{
name: "Golden Gate Pizza",
avg_rating: 4.5
},
{
name: "Venice Pizza",
avg_rating: 3.0
}
N hàng đầu cho mỗi nhóm (Truy vấn con có giới hạn)
Truy vấn sau đây tìm nạp tất cả các tài liệu từ nhóm bộ sưu tập restaurants và sử dụng một truy vấn con tương quan để tìm nạp 2 bài đánh giá được xếp hạng cao nhất cho từng nhà hàng.
Điều này đảm bảo rằng mảng bài đánh giá không tăng quá lớn và đạt đến giới hạn bộ nhớ của truy vấn.
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")));
Đáp
{
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" }
]
}
Kết hợp bộ sưu tập con
Truy vấn sau đây quét bộ sưu tập cities và sử dụng giai đoạn
subcollection(...) để kết hợp ngầm ẩn
trên các tài liệu từ một bộ sưu tập lồng nhau nhằm tìm số lượng nhà hàng cho mỗi
thành phố.
Node.js
let results = await execute(db.pipeline()
.collection("cities")
.addFields(subcollection("restaurants")
.toArrayExpression()
.length()
.as("restaurant_count")));
Đáp
{
__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
}
Thể hiện nhiều điều kiện kết hợp
Truy vấn sau đây quét nhóm bộ sưu tập restaurants và thực hiện phép kết hợp nhiều trường với nhóm bộ sưu tập reviews để tìm những chủ sở hữu đang đánh giá nhà hàng của chính họ.
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)));
Đáp
{
__name__: cities/SF/restaurants/X9An0HIlx29A9GPuRthS,
name: "Sunset Taco",
type: "mexican",
owner_id: "Edward"
}
Chống kết hợp (NOT EXISTS)
Truy vấn sau đây quét nhóm bộ sưu tập restaurants và tìm tất cả các nhà hàng chưa có bài đánh giá nào.
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)));
Đáp
{
__name__: "cities/LA/restaurants/X9An0HIlx29A9GPuRthS",
name: "Hollywood Sushi",
type: "sushi",
owner_id: "Ken Kenji"
}
Truy vấn con dưới dạng kết hợp
Truy vấn sau đây làm phẳng mối quan hệ giữa từng địa điểm bán bánh pizza và các bài đánh giá của địa điểm đó. Bằng cách đặt truy vấn con bên trong giai đoạn
unnest(...), máy chủ sẽ sao chép tài liệu nhà hàng bên ngoài
cho mỗi bài đánh giá phù hợp, tạo ra các tài liệu được kết hợp phẳng
(tương tự như 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")));
Đáp
{
__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" }
}
Truy vấn con không tương quan dưới dạng bộ lọc
Truy vấn sau đây trên bộ sưu tập reviews thực hiện các bộ lọc bằng cách sử dụng một truy vấn con không tương quan trên chính nó để tìm các bài đánh giá có điểm xếp hạng cao hơn điểm xếp hạng trung bình.
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");
Đáp
{
rating: 5,
reviewer_id "Alice"
},
{
rating: 5,
reviewer_id "Diana"
},
{
rating: 5,
reviewer_id "Hannah"
},
{
rating: 5,
reviewer_id "Julia"
}
Các phương pháp hay nhất
- Quản lý bộ nhớ bằng
toArrayExpression(): Hãy thận trọng vớitoArrayExpression()các truy vấn con, vì việc hiện thực hoá một số lượng lớn tài liệu có thể làm cạn kiệt giới hạn bộ nhớ truy vấn (128 MiB). Để giảm thiểu vấn đề này, hãy sử dụngselect(...)trong truy vấn con để chỉ trả về các trường cần thiết và áp dụng bộ lọcwhere(...)để giới hạn số lượng tài liệu được trả về. Hãy cân nhắc sử dụnglimit(...)nếu thích hợp để giới hạn số lượng tài liệu được truy vấn con trả về. - Lập chỉ mục: Đảm bảo rằng các trường được sử dụng trong mệnh đề
where(...)của truy vấn con được lập chỉ mục. Các phép kết hợp hiệu suất cao dựa vào khả năng thực hiện tìm kiếm chỉ mục thay vì quét toàn bộ bảng.
Để biết thêm các phương pháp hay nhất về truy vấn, hãy tham khảo hướng dẫn của chúng tôi về việc tối ưu hoá truy vấn.
Hạn chế
subcollection(...)phạm vi: Giai đoạn đầu vàosubcollection(...)chỉ được hỗ trợ trong các truy vấn con, vì giai đoạn này yêu cầu ngữ cảnh của một tài liệu mẹ để giải quyết mối quan hệ phân cấp và thực hiện phép kết hợp.- Độ sâu lồng nhau: Bạn có thể lồng các truy vấn con tối đa 20 lớp.
- Mức sử dụng bộ nhớ: Giới hạn 128 MiB đối với dữ liệu được hiện thực hoá áp dụng cho toàn bộ truy vấn, bao gồm cả tất cả các tài liệu được kết hợp.