Hiểu về lỗi này
Hãy coi toán tử $merge như là vạch đích cho dữ liệu của bạn. Trong MongoDB, hầu hết các giai đoạn (stage) aggregation sẽ biến đổi tài liệu và chuyển chúng xuống bước tiếp theo. Tuy nhiên, $merge là một hành động xuất dữ liệu. Nó ghi kết quả cuối cùng trực tiếp vào một collection. Vì lý do này, MongoDB thực thi một quy tắc nghiêm ngặt: $merge phải là giai đoạn cuối cùng tuyệt đối trong mảng aggregation của bạn.
Nếu bạn cố gắng thêm $sort, $project, hoặc thậm chí là một $limit đơn giản sau nó, database engine sẽ ngăn chặn bạn. Bạn sẽ thấy thông báo lỗi cụ thể này:
MongoServerError: The $merge stage must be the last stage in the pipeline
Tại sao lỗi này xảy ra
Aggregation framework hoạt động giống như một băng chuyền. Mỗi giai đoạn xử lý dữ liệu và chuyển giao cho giai đoạn kế tiếp. Giai đoạn $merge rất đặc biệt vì nó tương tác với storage engine để thực hiện các tác vụ nặng như upsert hoặc thay thế tài liệu.
Khi dữ liệu đã đến collection đích, nhiệm vụ của pipeline kết thúc. MongoDB không cho phép bạn tiếp tục xử lý các tài liệu đã được ghi xuống đĩa. Lỗi này thường xuất hiện vì ba lý do:
- Vô tình thêm stage: Bạn đang sử dụng
.push()trong một vòng lặp JavaScript để xây dựng pipeline và đã thêm một stage sau logic merge. - Lỗi do sao chép mẫu: Bạn đã sao chép một khối
$projecttừ một truy vấn khác và dán nó vào cuối mã nguồn của mình. - Lọc dữ liệu sau khi ghi: Bạn nhầm tưởng rằng mình có thể lọc kết quả cho giao diện người dùng (UI) sau khi đã ghi chúng vào cơ sở dữ liệu.
Cách khắc phục
1. Xác định giai đoạn bị sai thứ tự
Kiểm tra mảng aggregation của bạn để tìm bất kỳ dấu ngoặc nhọn {} nào xuất hiện sau khối $merge. Ngay cả một stage $count nhỏ ở cuối cũng sẽ làm hỏng truy vấn. Đây là một ví dụ về một pipeline bị lỗi:
db.orders.aggregate([
{ $match: { status: "shipped" } },
{ $group: { _id: "$product_id", unitsSold: { $sum: "$quantity" } } },
{
$merge: {
into: "sales_report",
whenMatched: "replace"
}
},
{ $sort: { unitsSold: -1 } } // LỖI: Bạn không thể sắp xếp sau khi đã merge
])
2. Sắp xếp lại Pipeline của bạn
Khắc phục vấn đề này thường đơn giản là di chuyển stage bị đặt sai vị trí. Nếu bạn cần sắp xếp 10.000 tài liệu trước khi lưu chúng, hãy đặt stage $sort trước stage $merge. Nếu bạn cần xem kết quả trong ứng dụng của mình, hãy chạy một truy vấn find() riêng biệt trên collection đích sau khi aggregation hoàn tất.
Đây là phiên bản đã sửa lỗi:
db.orders.aggregate([
{ $match: { status: "shipped" } },
{ $group: { _id: "$product_id", unitsSold: { $sum: "$quantity" } } },
{ $sort: { unitsSold: -1 } }, // Đúng: Dữ liệu được sắp xếp trước khi ghi
{
$merge: {
into: "sales_report",
whenMatched: "replace",
whenNotMatched: "insert"
}
}
])
3. Cẩn thận với logic động (Node.js)
Khi xây dựng pipeline bằng mã lập trình, thứ tự thực thi trong mã của bạn rất quan trọng. Một sai lầm phổ biến trong Node.js trông như thế này:
const pipeline = [
{ $match: { year: 2023 } }
];
// Logic thêm stage merge
pipeline.push({ $merge: { into: "yearly_archive" } });
// Một hàm khác sau đó thêm một projection
pipeline.push({ $project: { _id: 0 } }); // Điều này gây ra lỗi crash
// Cách sửa: Luôn đảm bảo logic merge là phần tử cuối cùng được push vào mảng.
Kiểm tra giải pháp
Chạy aggregation đã sửa của bạn. Lưu ý rằng $merge không trả về danh sách tài liệu cho console; nó thường trả về một tập kết quả rỗng hoặc metadata thông báo thành công. Để xác minh dữ liệu, hãy truy vấn trực tiếp collection đích:
// 1. Chạy aggregation trước
db.orders.aggregate([...]);
// 2. Kiểm tra dữ liệu trong collection mới
db.sales_report.find().limit(10);
Mẹo nâng cao
$merge so với $out
Cả hai giai đoạn này đều phải nằm ở cuối pipeline. Tuy nhiên, $merge linh hoạt hơn nhiều. Trong khi $out xóa collection hiện có và thay thế nó hoàn toàn, $merge có thể cập nhật các trường cụ thể trong các tài liệu hiện có. Hãy sử dụng $merge cho các tập dữ liệu lớn khi bạn chỉ cần đồng bộ hóa các thay đổi thay vì xây dựng lại toàn bộ bảng.
Hiệu suất và Bộ nhớ
Đặt một $sort trước $merge là hoàn toàn hợp lệ, nhưng hãy cẩn thận với các tập dữ liệu lớn. Nếu việc sắp xếp của bạn vượt quá giới hạn 100MB RAM và bạn chưa đánh index cho các trường đó, pipeline sẽ thất bại. Hãy luôn cố gắng sử dụng stage $match sớm để giảm số lượng tài liệu trước khi dữ liệu đến giai đoạn merge cuối cùng.

