Jak bylo uvedeno dříve v komentáři, k chybě dochází, protože při provádění $lookup
které ve výchozím nastavení vytváří cílové "pole" v rámci nadřazeného dokumentu z výsledků cizí kolekce, celková velikost dokumentů vybraných pro toto pole způsobí, že nadřazený objekt překročí limit 16 MB BSON.
Počítadlo pro toto je zpracování pomocí $unwind
který bezprostředně následuje za $lookup
fáze potrubí. To ve skutečnosti mění chování $lookup
v tom, že namísto vytváření pole v nadřazeném prvku jsou výsledky místo toho „kopií“ každého rodiče pro každý odpovídající dokument.
Skoro jako při běžném používání $unwind
s tou výjimkou, že namísto zpracování jako "samostatné" fáze potrubí se unwinding
akce je ve skutečnosti přidána do $lookup
samotný provoz potrubí. V ideálním případě také postupujte podle $unwind
s $match
podmínkou, která také vytvoří matching
argument, který má být také přidán do $lookup
. Ve skutečnosti to můžete vidět v explain
výstup pro potrubí.
Toto téma je ve skutečnosti pokryto (stručně) v části Optimalizace agregačního potrubí v základní dokumentaci:
$lookup + $unwind Coalescence
Novinka ve verzi 3.2.
Když $unwind bezprostředně následuje po dalším $lookup a $unwind působí na poli jako $lookup, optimalizátor může sloučit $unwind do fáze $lookup. Vyhnete se tak vytváření velkých mezilehlých dokumentů.
Nejlépe se to demonstruje na výpisu, který vystavuje server stresu vytvářením „souvisejících“ dokumentů, které by překročily limit 16 MB BSON. Provedeno co nejkratší dobu, aby došlo k porušení a obcházení limitu 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();
}
})();
Po vložení některých počátečních dat se zápis pokusí spustit souhrn obsahující pouze $lookup
který selže s následující chybou:
{ MongoError:Celková velikost dokumentů v kanálu shody okrajů { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } překračuje maximální velikost dokumentu
Což v podstatě znamená, že limit BSON byl při načítání překročen.
Naproti tomu další pokus přidá $unwind
a $match
etapy potrubí
Výstup Vysvětlení :
{
"$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"
}
}
}
A tento výsledek samozřejmě uspěje, protože protože výsledky již nejsou umísťovány do nadřazeného dokumentu, nelze překročit limit BSON.
To se opravdu děje jako výsledek přidání $unwind
pouze, ale $match
je například přidáno, aby se ukázalo, že je to také přidáno do $lookup
fázi a že celkovým efektem je "omezení" výsledků vrácených efektivním způsobem, protože vše se děje v tomto $lookup
operace a ve skutečnosti nejsou vráceny žádné jiné výsledky než ty, které se shodují.
Konstruováním tímto způsobem se můžete dotazovat na „referenční data“, která by překročila limit BSON, a poté, chcete-li, $group
výsledky zpět do formátu pole, jakmile byly efektivně filtrovány „skrytým dotazem“, který ve skutečnosti provádí $lookup
.
MongoDB 3.6 a vyšší – další pro „LEFT JOIN“
Jak poznamenává veškerý obsah výše, limit BSON je "tvrdý" limit, který nemůžete překročit, a to je obecně důvod, proč $unwind
je nezbytný jako přechodný krok. Existuje však omezení, že „LEFT JOIN“ se stane „INNER JOIN“ na základě $unwind
kde nemůže zachovat obsah. Také dokonce preserveNulAndEmptyArrays
by negovalo "slučování" a stále by zůstalo nedotčené pole, což by způsobilo stejný problém s limitem BSON.
MongoDB 3.6 přidává novou syntaxi do $lookup
který umožňuje použití výrazu "podpotrubí" místo "místního" a "cizího" klíče. Takže namísto použití možnosti „slučování“, jak bylo ukázáno, pokud vytvořené pole také neporuší limit, je možné vložit podmínky do tohoto potrubí, které vrátí pole „neporušené“ a možná bez žádných shod, jak by bylo indikativní. z "LEVÉHO PŘIPOJENÍ".
Nový výraz by pak byl:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
Ve skutečnosti by to bylo v podstatě to, co MongoDB dělá "pod pokličkou" s předchozí syntaxí od 3.6 používá $expr
„interně“ za účelem sestavení prohlášení. Rozdíl je samozřejmě v tom, že zde není žádné "unwinding"
možnost přítomná ve způsobu $lookup
skutečně bude vykonán.
Pokud se v důsledku "pipeline"
ve skutečnosti nevytvoří žádné dokumenty výraz, pak bude cílové pole v hlavním dokumentu ve skutečnosti prázdné, stejně jako "LEFT JOIN" ve skutečnosti dělá a bylo by normálním chováním $lookup
bez dalších možností.
Výstupní pole však NESMÍ způsobit, že dokument, ve kterém se vytváří, překročí limit BSON . Je tedy skutečně na vás, abyste zajistili, že jakýkoli „odpovídající“ obsah podle podmínek zůstane pod tímto limitem nebo bude stejná chyba přetrvávat, pokud samozřejmě nepoužijete $unwind
k provedení „INNER JOIN“.