Như đã nêu trước đó trong nhận xét, lỗi xảy ra do khi thực hiện $lookup
theo mặc định tạo ra một "mảng" đích trong tài liệu mẹ từ kết quả của bộ sưu tập nước ngoài, tổng kích thước của tài liệu được chọn cho mảng đó khiến cho tài liệu gốc vượt quá Giới hạn 16MB BSON.
Bộ đếm cho điều này là xử lý với một $unwind
ngay sau $lookup
giai đoạn đường ống. Điều này thực sự thay đổi hành vi của $lookup
sao cho thay vì tạo ra một mảng ở vùng gốc, thay vào đó, kết quả là "bản sao" của mỗi vùng gốc cho mọi tài liệu được so khớp.
Khá giống như việc sử dụng $unwind
thông thường , ngoại trừ việc thay vì xử lý như một giai đoạn đường dẫn "riêng biệt", thì unwinding
hành động thực sự được thêm vào $lookup
hoạt động của đường ống chính nó. Tốt nhất là bạn cũng làm theo $unwind
với $match
điều kiện này cũng tạo ra một matching
đối số cũng được thêm vào $lookup
. Bạn thực sự có thể thấy điều này trong explain
đầu ra cho đường ống.
Chủ đề thực sự được đề cập (ngắn gọn) trong phần Tối ưu hóa đường ống tổng hợp trong tài liệu cốt lõi:
$ lookup + $ unwind Coalescence
Mới trong phiên bản 3.2.
Khi $ unwind ngay sau một lần tra cứu $ khác và $ unwind hoạt động trên trường như của $ lookup, trình tối ưu hóa có thể kết hợp $ unwind vào giai đoạn tra cứu $. Điều này tránh tạo ra các tài liệu trung gian lớn.
Được thể hiện tốt nhất với một danh sách khiến máy chủ bị căng thẳng bằng cách tạo các tài liệu "có liên quan" vượt quá giới hạn 16MB BSON. Thực hiện càng nhanh càng tốt để vừa phá vỡ vừa khắc phục được Giới hạn BSON:
const MongoClient = require('mongodb').MongoClient;
const uri = 'mongodb://localhost/test';
function data(data) {
console.log(JSON.stringify(data, undefined, 2))
}
(async function() {
let db;
try {
db = await MongoClient.connect(uri);
console.log('Cleaning....');
// Clean data
await Promise.all(
["source","edge"].map(c => db.collection(c).remove() )
);
console.log('Inserting...')
await db.collection('edge').insertMany(
Array(1000).fill(1).map((e,i) => ({ _id: i+1, gid: 1 }))
);
await db.collection('source').insert({ _id: 1 })
console.log('Fattening up....');
await db.collection('edge').updateMany(
{},
{ $set: { data: "x".repeat(100000) } }
);
// The full pipeline. Failing test uses only the $lookup stage
let pipeline = [
{ $lookup: {
from: 'edge',
localField: '_id',
foreignField: 'gid',
as: 'results'
}},
{ $unwind: '$results' },
{ $match: { 'results._id': { $gte: 1, $lte: 5 } } },
{ $project: { 'results.data': 0 } },
{ $group: { _id: '$_id', results: { $push: '$results' } } }
];
// List and iterate each test case
let tests = [
'Failing.. Size exceeded...',
'Working.. Applied $unwind...',
'Explain output...'
];
for (let [idx, test] of Object.entries(tests)) {
console.log(test);
try {
let currpipe = (( +idx === 0 ) ? pipeline.slice(0,1) : pipeline),
options = (( +idx === tests.length-1 ) ? { explain: true } : {});
await new Promise((end,error) => {
let cursor = db.collection('source').aggregate(currpipe,options);
for ( let [key, value] of Object.entries({ error, end, data }) )
cursor.on(key,value);
});
} catch(e) {
console.error(e);
}
}
} catch(e) {
console.error(e);
} finally {
db.close();
}
})();
Sau khi chèn một số dữ liệu ban đầu, danh sách sẽ cố gắng chạy một tổng hợp chỉ bao gồm $lookup
sẽ không thành công với lỗi sau:
{MongoError:Tổng kích thước của tài liệu trong đường dẫn khớp cạnh {$ match:{$ và:[{gid:{$ eq:1}}, {}]}} vượt quá kích thước tài liệu tối đa
Về cơ bản, điều này cho bạn biết giới hạn BSON đã bị vượt quá khi truy xuất.
Ngược lại, lần thử tiếp theo sẽ thêm $unwind
và $match
các giai đoạn đường ống
Đầu ra Giải thích :
{
"$lookup": {
"from": "edge",
"as": "results",
"localField": "_id",
"foreignField": "gid",
"unwinding": { // $unwind now is unwinding
"preserveNullAndEmptyArrays": false
},
"matching": { // $match now is matching
"$and": [ // and actually executed against
{ // the foreign collection
"_id": {
"$gte": 1
}
},
{
"_id": {
"$lte": 5
}
}
]
}
}
},
// $unwind and $match stages removed
{
"$project": {
"results": {
"data": false
}
}
},
{
"$group": {
"_id": "$_id",
"results": {
"$push": "$results"
}
}
}
Và kết quả đó tất nhiên thành công, vì khi kết quả không còn được đặt vào tài liệu mẹ thì không thể vượt quá giới hạn BSON.
Điều này thực sự chỉ xảy ra do thêm $unwind
chỉ, trừ $match
được thêm vào chẳng hạn để cho thấy rằng đây cũng là được thêm vào $lookup
và hiệu quả tổng thể là "giới hạn" kết quả trả về một cách hiệu quả, vì tất cả được thực hiện trong $lookup
đó hoạt động và không có kết quả nào khác ngoài những kết quả phù hợp thực sự được trả về.
Bằng cách xây dựng theo cách này, bạn có thể truy vấn "dữ liệu được tham chiếu" sẽ vượt quá giới hạn BSON và sau đó nếu bạn muốn $group
kết quả trở lại định dạng mảng, sau khi chúng đã được lọc hiệu quả bởi "truy vấn ẩn" đang thực sự được thực hiện bởi $lookup
.
MongoDB 3.6 trở lên - Bổ sung cho "THAM GIA TRÁI"
Như tất cả nội dung ở trên lưu ý, Giới hạn BSON là một "khó" giới hạn mà bạn không thể vi phạm và đây thường là lý do tại sao $unwind
là cần thiết như một bước tạm thời. Tuy nhiên, có hạn chế là "LEFT JOIN" trở thành "INNER JOIN" nhờ $unwind
nơi nó không thể bảo toàn nội dung. Ngoài ra, ngay cả preserveNulAndEmptyArrays
sẽ phủ định "sự kết hợp" và vẫn để lại mảng nguyên vẹn, gây ra cùng một vấn đề Giới hạn BSON.
MongoDB 3.6 thêm cú pháp mới vào $lookup
cho phép biểu thức "đường ống con" được sử dụng thay cho các khóa "cục bộ" và "ngoại". Vì vậy, thay vì sử dụng tùy chọn "kết hợp" như đã trình bày, miễn là mảng được tạo ra cũng không vi phạm giới hạn, thì có thể đặt các điều kiện trong đường dẫn đó để trả về mảng "nguyên vẹn" và có thể không có kết quả phù hợp nào như biểu thị của "THAM GIA TRÁI".
Biểu thức mới sau đó sẽ là:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
Trên thực tế, về cơ bản đây sẽ là những gì MongoDB đang làm "chui" với cú pháp trước đó kể từ 3.6 sử dụng $expr
"nội bộ" để xây dựng câu lệnh. Sự khác biệt tất nhiên là không có "unwinding"
tùy chọn hiển thị trong cách $lookup
thực sự được thực thi.
Nếu không có tài liệu nào thực sự được tạo ra do "pipeline"
biểu thức, thì mảng đích trong tài liệu chính trên thực tế sẽ trống, giống như "LEFT JOIN" thực sự làm và sẽ là hành vi bình thường của $lookup
mà không có bất kỳ tùy chọn nào khác.
Tuy nhiên, mảng đầu ra KHÔNG PHẢI khiến tài liệu nơi nó đang được tạo vượt quá Giới hạn BSON . Vì vậy, việc đảm bảo rằng mọi nội dung "phù hợp" theo các điều kiện vẫn nằm trong giới hạn này hoặc lỗi tương tự sẽ vẫn tiếp diễn, tất nhiên là trừ khi bạn thực sự sử dụng $unwind
để tạo hiệu ứng "INNER JOIN".