Seperti yang dinyatakan sebelumnya dalam komentar, kesalahan terjadi karena saat melakukan $lookup
yang secara default menghasilkan "array" target dalam dokumen induk dari hasil koleksi asing, ukuran total dokumen yang dipilih untuk larik tersebut menyebabkan induk melebihi Batas BSON 16MB.
Penghitung untuk ini adalah memproses dengan $unwind
yang langsung mengikuti $lookup
tahap pipa. Ini sebenarnya mengubah perilaku $lookup
sedemikian rupa sehingga alih-alih menghasilkan larik di induk, hasilnya malah menjadi "salinan" dari setiap induk untuk setiap dokumen yang cocok.
Hampir seperti penggunaan biasa $unwind
, dengan pengecualian bahwa alih-alih memproses sebagai tahap saluran pipa "terpisah", unwinding
tindakan sebenarnya ditambahkan ke $lookup
pengoperasian pipa itu sendiri. Idealnya Anda juga mengikuti $unwind
dengan $match
kondisi, yang juga membuat matching
argumen untuk juga ditambahkan ke $lookup
. Anda sebenarnya dapat melihat ini di explain
keluaran untuk pipeline.
Topik ini sebenarnya dibahas (secara singkat) di bagian Pengoptimalan Pipa Agregasi dalam dokumentasi inti:
$lookup + $unwind Coalescence
Baru di versi 3.2.
Ketika $unwind segera mengikuti $lookup lain, dan $unwind beroperasi pada bidang as dari $lookup, pengoptimal dapat menggabungkan $unwind ke tahap $lookup. Ini menghindari pembuatan dokumen perantara berukuran besar.
Paling baik ditunjukkan dengan daftar yang menempatkan server di bawah tekanan dengan membuat dokumen "terkait" yang akan melebihi batas BSON 16 MB. Dilakukan sesingkat mungkin untuk memecahkan dan mengatasi Batas 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();
}
})();
Setelah memasukkan beberapa data awal, cantuman akan mencoba menjalankan agregat yang hanya terdiri dari $lookup
yang akan gagal dengan kesalahan berikut:
{ MongoError:Ukuran total dokumen dalam saluran pencocokan tepi { $match:{ $and :[ { gid:{ $eq:1 } }, {} ] } } melebihi ukuran dokumen maksimum
Yang pada dasarnya memberi tahu Anda bahwa batas BSON terlampaui saat pengambilan.
Sebaliknya, upaya berikutnya menambahkan $unwind
dan $match
tahapan pipa
Keluaran Jelaskan :
{
"$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"
}
}
}
Dan hasil tersebut tentu saja berhasil, karena hasil tidak lagi ditempatkan pada dokumen induk maka batas BSON tidak dapat dilampaui.
Ini benar-benar terjadi sebagai akibat dari menambahkan $unwind
saja, tetapi $match
ditambahkan misalnya untuk menunjukkan bahwa ini juga ditambahkan ke $lookup
tahap dan bahwa efek keseluruhannya adalah untuk "membatasi" hasil yang dikembalikan dengan cara yang efektif, karena semuanya dilakukan di $lookup
operasi dan tidak ada hasil lain selain yang cocok yang benar-benar dikembalikan.
Dengan membangun dengan cara ini Anda dapat meminta "data referensi" yang akan melebihi batas BSON dan kemudian jika Anda ingin $group
hasilnya kembali ke format array, setelah difilter secara efektif oleh "kueri tersembunyi" yang sebenarnya dilakukan oleh $lookup
.
MongoDB 3.6 ke atas - Tambahan untuk "LEFT JOIN"
Seperti yang dicatat oleh semua konten di atas, Batas BSON adalah "keras" batas yang tidak dapat Anda langgar dan inilah alasan mengapa $unwind
diperlukan sebagai langkah sementara. Namun ada batasan bahwa "LEFT JOIN" menjadi "INNER JOIN" berdasarkan $unwind
di mana ia tidak dapat mempertahankan konten. Bahkan preserveNulAndEmptyArrays
akan meniadakan "perpaduan" dan masih meninggalkan array yang utuh, menyebabkan masalah Batas BSON yang sama.
MongoDB 3.6 menambahkan sintaks baru ke $lookup
yang memungkinkan ekspresi "sub-pipa" digunakan sebagai pengganti kunci "lokal" dan "asing". Jadi, alih-alih menggunakan opsi "perpaduan" seperti yang ditunjukkan, selama larik yang dihasilkan tidak juga melanggar batas, dimungkinkan untuk menempatkan kondisi dalam saluran itu yang mengembalikan larik "utuh", dan mungkin tanpa kecocokan seperti yang akan menjadi indikasi dari "LEFT JOIN".
Ekspresi baru akan menjadi:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
Sebenarnya ini pada dasarnya adalah apa yang MongoDB lakukan "di balik selimut" dengan sintaks sebelumnya sejak 3.6 menggunakan $expr
"internal" untuk membangun pernyataan. Bedanya tentu tidak ada "unwinding"
opsi hadir dalam cara $lookup
benar-benar dieksekusi.
Jika tidak ada dokumen yang benar-benar dihasilkan sebagai hasil dari "pipeline"
ekspresi, maka array target dalam dokumen master sebenarnya akan kosong, seperti halnya "LEFT JOIN" yang sebenarnya dan akan menjadi perilaku normal $lookup
tanpa pilihan lain.
Namun larik keluaran ke TIDAK HARUS menyebabkan dokumen yang sedang dibuat melebihi Batas BSON . Jadi terserah Anda untuk memastikan bahwa konten "cocok" apa pun dengan ketentuan tetap di bawah batas ini atau kesalahan yang sama akan tetap ada, kecuali tentu saja Anda benar-benar menggunakan $unwind
untuk melakukan "INNER JOIN".