Както беше посочено по-рано в коментар, грешката възниква, защото при извършване на $lookup
който по подразбиране създава целеви „масив“ в родителския документ от резултатите от чуждата колекция, общият размер на документите, избрани за този масив, кара родителя да надвиши ограничението от 16MB BSON.
Броячът за това е да се обработва с $unwind
който непосредствено следва $lookup
етап на тръбопровода. Това всъщност променя поведението на $lookup
в така, че вместо да създават масив в родителя, резултатите са вместо това "копие" на всеки родител за всеки съпоставен документ.
Почти като редовното използване на $unwind
, с изключението, че вместо да се обработва като "отделен" етап на конвейера, unwinding
действието всъщност се добавя към $lookup
самата работа на тръбопровода. В идеалния случай трябва да следвате и $unwind
с $match
условие, което също създава matching
аргумент също да бъде добавен към $lookup
. Всъщност можете да видите това в explain
изход за тръбопровода.
Темата всъщност е разгледана (накратко) в раздел за оптимизация на агрегационния конвейер в основната документация:
$lookup + $unwind Coalescence
Ново във версия 3.2.
Когато $unwind непосредствено следва друго $lookup и $unwind оперира с полето as на $lookup, оптимизаторът може да обедини $unwind в етап $lookup. Това избягва създаването на големи междинни документи.
Най-добре се демонстрира с списък, който поставя сървъра в стрес, като създава "свързани" документи, които биха надвишили ограничението от 16MB BSON. Направено възможно най-кратко за прекъсване и заобикаляне на 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();
}
})();
След вмъкване на някои първоначални данни, списъкът ще се опита да изпълни агрегат, който се състои само от $lookup
което ще се провали със следната грешка:
{ MongoError:Общият размер на документите в конвейера за съвпадение на ръбове { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } надвишава максималния размер на документа
Което по същество ви казва, че ограничението на BSON е надвишено при извличане.
За разлика от това следващият опит добавя $unwind
и $match
тръбопроводни етапи
Изходът Explain :
{
"$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"
}
}
}
И този резултат, разбира се, е успешен, защото тъй като резултатите вече не се поставят в родителския документ, ограничението на BSON не може да бъде надвишено.
Това наистина се случва просто в резултат на добавяне на $unwind
само, но $match
се добавя например, за да покаже, че това е също добавен в $lookup
етап и че цялостният ефект е „ограничаване“ на резултатите, върнати по ефективен начин, тъй като всичко се прави в този $lookup
операция и действително не се връщат никакви други резултати, освен съвпадащите.
Чрез конструиране по този начин можете да потърсите "референтни данни", които биха надвишили ограничението на BSON и след това, ако искате $group
резултатите обратно във формат на масив, след като бъдат ефективно филтрирани от "скритата заявка", която всъщност се изпълнява от $lookup
.
MongoDB 3.6 и по-горе – Допълнително за „LEFT JOIN“
Както отбелязва цялото съдържание по-горе, ограничението на BSON е "твърдо" ограничение, което не можете да нарушите и това обикновено е причината $unwind
е необходимо като междинна стъпка. Съществува обаче ограничението, че „LEFT JOIN“ става „INNER JOIN“ по силата на $unwind
където не може да запази съдържанието. Също така дори preserveNulAndEmptyArrays
ще отрече „сливането“ и пак ще остави непокътнатия масив, причинявайки същия проблем с BSON Limit.
MongoDB 3.6 добавя нов синтаксис към $lookup
което позволява да се използва израз "под-тръбопровод" вместо "локалния" и "чуждия" ключ. Така че вместо да се използва опцията "coalescence", както е показано, стига произведеният масив също да не наруши границата, е възможно да се поставят условия в този конвейер, който връща масива "непокътнат" и вероятно без съвпадения, което би било показателно на „LEFT JOIN“.
Тогава новият израз ще бъде:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
Всъщност това би било основно това, което MongoDB прави „под завивките“ с предишния синтаксис от 3.6 използва $expr
"вътрешно", за да се конструира изявлението. Разликата, разбира се, е, че няма "unwinding"
опция присъства в как $lookup
всъщност се изпълнява.
Ако действително не се произвеждат документи в резултат на "pipeline"
израз, тогава целевият масив в основния документ всъщност ще бъде празен, точно както всъщност прави "LEFT JOIN" и би било нормалното поведение на $lookup
без никакви други опции.
Въпреки това изходният масив на НЕ ТРЯБВА да кара документа, където се създава, да надвишава BSON лимита . Така че наистина зависи от вас да гарантирате, че всяко "съвпадащо" съдържание от условията остава под това ограничение или същата грешка ще продължи, освен ако, разбира се, всъщност не използвате $unwind
за да осъществите „INNER JOIN“.