我有一个非常简单的$lookup
聚合查询,如下所示:
{'$lookup':
{'from': 'edge',
'localField': 'gid',
'foreignField': 'to',
'as': 'from'}}
当我在匹配足够文档时运行此操作时,我收到以下错误:
Command failed with error 4568: 'Total size of documents in edge
matching { $match: { $and: [ { from: { $eq: "geneDatabase:hugo" }
}, {} ] } } exceeds maximum document size' on server
所有限制文档数量的尝试都失败了。 allowDiskUse: true
什么也没做。发送cursor
无效。在聚合中添加$limit
也会失败。
怎么会这样?
然后我再次看到错误。 $match
和$and
以及$eq
来自哪里?幕后的聚合管道是否会将$lookup
调用另一个聚合,一个单独运行,我无法为游标提供限制或使用游标?
这里发生了什么?
答案 0 :(得分:23)
如前面评论中所述,发生错误是因为执行$lookup
时默认情况下生成目标"数组"在外部集合的结果的父文档中,为该数组选择的文档的总大小会导致父项超过16MB BSON Limit.
此计数器将使用$unwind
进行处理,该$lookup
紧跟在$lookup
管道阶段之后。这实际上改变了$unwind
的行为,使得不是在父节点中生成数组,而是在" copy"每个匹配的文件的每个父母。
就像常规使用$lookup
一样,除了处理为"单独的"在管道阶段,unwinding
操作实际上已添加到$unwind
管道操作本身。理想情况下,您还会使用$match
条件跟$lookup
,这也会创建一个matching
参数,同时添加到Aggregation Pipeline Optimization。您实际上可以在管道的explain
输出中看到这一点。
在核心文档的$lookup
部分实际涵盖(简要)主题:
$ lookup + $ unwind Coalescence
3.2版中的新功能。
当$ unwind紧跟在另一个$ lookup之后,$ unwind在$ lookup的as字段上运行时,优化器可以将$ 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();
}
})();
插入一些初始数据后,列表将尝试运行仅由$unwind
组成的聚合,该聚合将失败,并显示以下错误:
{MongoError:边缘匹配管道中的文档总大小{$ match:{$ and:[{gid:{$ eq:1}},{}]}}超出最大文档大小
这基本上告诉你检索时超过了BSON限制。
相比之下,下一次尝试会添加$match
和$unwind
管道阶段
解释输出:
{
"$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限制。
这恰好仅在添加$match
后才会发生,但会添加$lookup
,例如显示还添加到$lookup
阶段和整体效果是'#34;限制"结果以有效的方式返回,因为它全部在$group
操作中完成,除了那些匹配之外没有其他结果实际返回。
通过这种方式构建,您可以查询"引用数据"超过BSON限制,然后如果你希望$lookup
结果回到数组格式,一旦它们被隐藏的查询"这实际上是由$unwind
执行的。
正如上面的所有内容所述,BSON限制是一个"硬" 限制,你不能违反,这通常是$unwind
作为一个必要的原因。临时步骤。然而,有限制是" LEFT JOIN"成为一个内心联盟"凭借$lookup
无法保留内容的地方。甚至preserveNulAndEmptyArrays
也会否定"合并"并且仍然保留完整的阵列,导致相同的BSON Limit问题。
MongoDB 3.6为$expr
增加了新的语法,允许一个"子流水线"表达式用于代替" local"和"外国"键。因此,而不是使用"合并"如所示的选项,只要生成的数组也没有超出限制,就可以在该管道中放置条件,返回数组"完整",并且可能没有匹配,这表示& #34; LEFT JOIN"。
新表达式将是:
{ "$lookup": {
"from": "edge",
"let": { "gid": "$gid" },
"pipeline": [
{ "$match": {
"_id": { "$gte": 1, "$lte": 5 },
"$expr": { "$eq": [ "$$gid", "$to" ] }
}}
],
"as": "from"
}}
事实上,这基本上就是MongoDB正在做的事情"在#34; 的基础上使用以前的语法,因为3.6使用$lookup
"内部"为了构建声明。当然,区别在于$lookup
实际执行的方式中没有"unwinding"
选项。
如果"pipeline"
表达式没有实际生成任何文档,那么主文档中的目标数组实际上将为空,就像" LEFT JOIN"实际上并且是$unwind
没有任何其他选项的正常行为。
然而输出数组绝不能导致创建它的文档超过BSON限制。因此,确保任何"匹配"条件下的内容保持在此限制之下或同样的错误将持续存在,除非您实际上使用{{3}}来实现" INNER JOIN"。
答案 1 :(得分:1)
我对延缓Node.js查询存在相同的问题,因为“赎回”集合具有超过40万个数据。我正在使用Mongo DB服务器4.2和Node JS驱动程序3.5.3。
db.collection('businesses').aggregate(
{
$lookup: { from: 'redemptions', localField: "_id", foreignField: "business._id", as: "redemptions" }
},
{
$project: {
_id: 1,
name: 1,
email: 1,
"totalredemptions" : {$size:"$redemptions"}
}
}
我对查询进行了如下修改,以使其超快工作。
db.collection('businesses').aggregate(query,
{
$lookup:
{
from: 'redemptions',
let: { "businessId": "$_id" },
pipeline: [
{ $match: { $expr: { $eq: ["$business._id", "$$businessId"] } } },
{ $group: { _id: "$_id", totalCount: { $sum: 1 } } },
{ $project: { "_id": 0, "totalCount": 1 } }
],
as: "redemptions"
},
{
$project: {
_id: 1,
name: 1,
email: 1,
"totalredemptions" : {$size:"$redemptions"}
}
}
}