Lỗi Gặp Phải
Bạn đang chạy một aggregation pipeline với $lookup trên một MongoDB cluster được sharded và gặp lỗi sau:
MongoServerError: $lookup from a sharded collection is not allowed
Nguyên nhân là collection foreign — collection được chỉ định trong from — đang được sharded. MongoDB có những quy tắc nghiêm ngặt về điều này, và các phiên bản cũ hơn đơn giản là từ chối thực hiện phép join.
Nguyên Nhân Gốc Rễ
MongoDB's $lookup cần giải quyết foreign collection cục bộ trên một node duy nhất. Khi collection đó được sharded, dữ liệu của nó sẽ được phân tán trên nhiều shard. Nếu không có chiến lược định tuyến cụ thể, MongoDB sẽ phải truy vấn tất cả các shard để hoàn thành phép join — một thao tác scatter-gather tác động đến tất cả N shard trong cluster của bạn.
Thay vì âm thầm thực hiện thao tác tốn kém đó, MongoDB chặn hoàn toàn.
Giới hạn cụ thể phụ thuộc vào phiên bản:
- MongoDB < 5.1:
$lookupđến một foreign collection được sharded là không được phép hoàn toàn. - MongoDB 5.1+: Được phép, nhưng chỉ trong các điều kiện cụ thể — dữ liệu cùng nằm trên một shard, hoặc cross-shard với những đánh đổi về hiệu suất.
Các nguyên nhân thường gặp:
- Chạy aggregation qua mongos với collection
fromđược sharded trong MongoDB < 5.1 - Collection nguồn không được sharded nhưng collection
fromlại được sharded - Sử dụng
$lookupbên trong$facetkhi một trong hai phía được sharded
Cách Khắc Phục: Nhiều Phương Án
Phương Án 1 — Nâng Cấp Lên MongoDB 5.1 Hoặc Cao Hơn (Khuyến Nghị Dài Hạn)
MongoDB 5.1 đã thêm hỗ trợ native cho $lookup trên các sharded collection. Trên phiên bản 4.x hoặc 5.x đầu, nâng cấp là con đường sạch nhất — không cần viết lại query, không cần thay đổi schema.
Kiểm tra phiên bản hiện tại của bạn trước:
mongosh --eval "db.version()"
Bị kẹt ở phiên bản cũ hơn hiện tại? Các phương án dưới đây hoạt động mà không cần nâng cấp.
Phương Án 2 — Kết Nối Trực Tiếp Đến Một Shard (Bỏ Qua mongos)
Bỏ qua mongos hoàn toàn và kết nối thẳng đến primary của một shard. Aggregation chạy cục bộ trên shard đó, nên giới hạn $lookup không còn áp dụng.
// Kết nối trực tiếp đến primary của shard
mongosh "mongodb://shard1-primary:27017/mydb"
db.orders.aggregate([
{
$lookup: {
from: "products",
localField: "product_id",
foreignField: "_id",
as: "product_info"
}
}
])
Lưu ý: Cách này chỉ hoạt động nếu dữ liệu của collection from thực sự nằm trên shard đó. Nếu bạn có 3 shard và products được phân tán trên tất cả, bạn sẽ nhận được kết quả không đầy đủ — mà không có cảnh báo. Hãy xác minh phân bố shard của bạn trước khi dùng cách này trên môi trường production.
Phương Án 3 — Bỏ Sharding Khỏi Foreign Collection
Không phải mọi collection trong một sharded database đều cần được sharded. Nếu collection from của bạn là một bảng tham chiếu nhỏ — danh mục sản phẩm, vai trò người dùng, dữ liệu cấu hình — hãy để nó không được sharded. Giới hạn chỉ áp dụng cho các foreign collection được sharded.
// Kiểm tra xem một collection có được sharded không
use config
db.collections.find({ _id: "mydb.products" })
Để xóa sharding, các tùy chọn phụ thuộc vào phiên bản. Trước 7.0, dump và restore mà không có shard key. Bắt đầu từ MongoDB 7.0, có một lệnh chuyên dụng:
// Chỉ dành cho MongoDB 7.0+
db.adminCommand({ unshardCollection: "mydb.products" })
Phương Án 4 — Dùng Dạng Pipeline Của $lookup (MongoDB 5.1+)
Cú pháp pipeline cung cấp cho query planner của MongoDB nhiều thông tin hơn để xử lý. Trên 5.1+, nó có thể sử dụng thông tin này để tối ưu hóa phép join với một sharded foreign collection:
db.orders.aggregate(
[
{
$lookup: {
from: "products",
let: { pid: "$product_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$_id", "$$pid"] } } }
],
as: "product_info"
}
}
],
{ allowDiskUse: true }
)
Việc khai báo rõ ràng điều kiện join cho phép MongoDB định tuyến đến đúng shard thay vì phát query đến tất cả các shard.
Phương Án 5 — Denormalize và Nhúng Dữ Liệu Lúc Ghi
Đối với các pipeline quan trọng về hiệu suất, hãy bỏ qua phép join hoàn toàn. Nhúng trực tiếp các trường bạn cần vào document nguồn lúc ghi — khi đọc sẽ không cần cross-collection lookup nào cả.
// Nhúng dữ liệu sản phẩm lúc insert thay vì join lúc đọc
db.orders.insertOne({
_id: ObjectId(),
product_id: "abc123",
product_snapshot: {
name: "Widget Pro",
price: 29.99,
sku: "WP-001"
}
})
Đúng là bạn đang sao chép một số dữ liệu. Nhưng trên một sharded cluster với hàng triệu đơn hàng, việc tránh các scatter-gather join trên mỗi lần đọc thường xứng đáng với sự đánh đổi đó.
Xác Minh Bản Sửa
Chạy aggregation với $limit: 1 để kiểm tra nhanh:
db.orders.aggregate([
{
$lookup: {
from: "products",
localField: "product_id",
foreignField: "_id",
as: "product_info"
}
},
{ $limit: 1 }
])
Không còn MongoServerError? Tốt. Ngay cả kết quả rỗng ([]) cũng có nghĩa là pipeline đã chạy thành công.
Tiếp theo, kiểm tra explain plan. Bạn muốn thấy "stage": "EQ_LOOKUP" — không phải SHARD_MERGE ở phía foreign, vì đó là dấu hiệu của một broadcast cross-shard tốn kém:
db.orders.explain("executionStats").aggregate([
{
$lookup: {
from: "products",
localField: "product_id",
foreignField: "_id",
as: "product_info"
}
}
])
Phòng Ngừa
- Quyết định chiến lược sharding trước khi shard. Các collection được dùng làm đích
fromtrong$lookupthường tốt hơn nếu để không được sharded — trừ khi chúng thực sự lớn (hàng chục triệu document). - Sao chép topology production trong môi trường staging. Môi trường dev single-node sẽ không phát hiện các giới hạn sharding. Hãy kiểm tra aggregation với một cluster khớp với layout shard của production.
- Hướng đến MongoDB 5.1+ cho các sharded deployment mới. Hỗ trợ cross-shard
$lookupmột mình đã đủ để việc nâng cấp xứng đáng. - Hãy nghĩ về các reference/lookup table như dimension table trong data warehouse — nhỏ, ổn định, và tốt hơn nếu để tập trung. Đừng shard chúng một cách phản xạ chỉ vì phần còn lại của database được sharded.
Mẹo Hay
Khi debug aggregation pipeline trên môi trường dev và production, các chỉnh sửa vô tình hoặc lỗi copy-paste trong pipeline JSON có thể gây ra sự khác biệt về hành vi rất khó truy vết. Tôi dùng ToolCraft's Hash Generator để so sánh hash của file pipeline giữa các môi trường — một cách nhanh chóng để loại trừ câu hỏi "file này có thực sự thay đổi không?" trước khi đi sâu hơn.

