Som nämnts tidigare i kommentaren uppstår felet eftersom $lookup
utförs som som standard producerar en mål "array" inom det överordnade dokumentet från resultaten av den främmande insamlingen, gör den totala storleken på dokument som valts för den arrayen att föräldern överskrider 16 MB BSON-gränsen.
Räknaren för detta är att bearbeta med en $unwind
som omedelbart följer $lookup
rörledningsstadiet. Detta ändrar faktiskt beteendet hos $lookup
på så sätt att istället för att producera en array i den överordnade, blir resultaten istället en "kopia" av varje överordnad för varje matchat dokument.
Ungefär som vanlig användning av $unwind
, med undantaget att istället för att behandla som ett "separat" pipelinesteg, unwinding
åtgärden läggs faktiskt till i $lookup
själva pipelinedriften. Helst följer du också $unwind
med en $match
condition, vilket också skapar en matching
argument som också ska läggas till i $lookup
. Du kan faktiskt se detta i explain
output för pipeline.
Ämnet behandlas faktiskt (kortfattat) i ett avsnitt av Aggregation Pipeline Optimization i kärndokumentationen:
$lookup + $unwind Coalescence
Nyhet i version 3.2.
När en $unwind omedelbart följer en annan $lookup, och $unwind fungerar på as-fältet för $lookup, kan optimeraren sammansmälta $unwind till $lookup-stadiet. Detta undviker att skapa stora mellanliggande dokument.
Bäst demonstreras med en lista som sätter servern under stress genom att skapa "relaterade" dokument som skulle överskrida 16MB BSON-gränsen. Görs så kort som möjligt för att både bryta och komma runt BSON-gränsen:
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();
}
})();
Efter att ha infogat några initiala data kommer listningen att försöka köra ett aggregat som bara består av $lookup
som kommer att misslyckas med följande fel:
{ MongoError:Total storlek på dokument i kantmatchande pipeline { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } överskrider den maximala dokumentstorleken
Vilket i princip talar om för dig att BSON-gränsen överskreds vid hämtning.
Däremot lägger nästa försök till $unwind
och $match
pipeline etapper
Utgången 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"
}
}
}
Och det resultatet lyckas förstås, för eftersom resultaten inte längre placeras i det överordnade dokumentet kan BSON-gränsen inte överskridas.
Detta händer egentligen bara som ett resultat av att $unwind
lagts till bara, men $match
läggs till till exempel för att visa att detta är också läggs till i $lookup
skede och att den övergripande effekten är att "begränsa" resultaten som returneras på ett effektivt sätt, eftersom det hela görs i den $lookup
operation och inga andra resultat än de som matchar returneras faktiskt.
Genom att konstruera på detta sätt kan du fråga efter "referensdata" som skulle överskrida BSON-gränsen och sedan om du vill ha $group
resultaten tillbaka till ett arrayformat när de väl har filtrerats av den "dolda frågan" som faktiskt utförs av $lookup
.
MongoDB 3.6 och ovan - Ytterligare för "LEFT JOIN"
Som allt innehåll ovan noterar är BSON-gränsen "hård" gräns som du inte kan bryta och det är i allmänhet därför som $unwind
är nödvändigt som ett interimistiskt steg. Det finns dock begränsningen att "LEFT JOIN" blir en "INNER JOIN" i kraft av $unwind
där den inte kan bevara innehållet. Även preserveNulAndEmptyArrays
skulle upphäva "sammansmältningen" och fortfarande lämna den intakta arrayen, vilket orsakar samma BSON Limit-problem.
MongoDB 3.6 lägger till ny syntax i $lookup
som tillåter ett "sub-pipeline"-uttryck att användas i stället för "lokala" och "främmande" nycklar. Så istället för att använda alternativet "koalescens" som visat, så länge som den producerade arrayen inte också bryter mot gränsen är det möjligt att sätta villkor i den pipeline som returnerar arrayen "intakt", och möjligen utan matchningar, vilket skulle vara indikativt av en "LEFT JOIN".
Det nya uttrycket skulle då vara:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
I själva verket skulle detta i princip vara vad MongoDB gör "under täcket" med föregående syntax sedan 3.6 använder $expr
"internt" för att konstruera uttalandet. Skillnaden är naturligtvis att det inte finns någon "unwinding"
alternativ som finns i hur $lookup
faktiskt avrättas.
Om inga dokument faktiskt produceras som ett resultat av "pipeline"
uttryck, då kommer målarrayen i huvuddokumentet i själva verket vara tom, precis som en "LEFT JOIN" faktiskt gör och skulle vara det normala beteendet för $lookup
utan några andra alternativ.
Men utdatamatrisen FÅR INTE göra att dokumentet där det skapas överskrider BSON-gränsen . Så det är verkligen upp till dig att se till att allt "matchande" innehåll enligt villkoren förblir under denna gräns, annars kommer samma fel att kvarstå, såvida du inte faktiskt använder $unwind
för att utföra "INNER JOIN".