Wex*_*Wex 26 mapreduce mongodb mongodb-query aggregation-framework
我有一组文件:
date: Date
users: [
{ user: 1, group: 1 }
{ user: 5, group: 2 }
]
date: Date
users: [
{ user: 1, group: 1 }
{ user: 3, group: 2 }
]
Run Code Online (Sandbox Code Playgroud)
我想查询这个集合,找到我的用户数组中每个用户id在另一个数组中的所有文档,[1,5,7].在此示例中,只有第一个文档匹配.
我能找到的最佳解决方案是:
$where: function() {
var ids = [1, 5, 7];
return this.users.every(function(u) {
return ids.indexOf(u.user) !== -1;
});
}
Run Code Online (Sandbox Code Playgroud)
不幸的是,这似乎伤害了性能在$ where docs中说明:
$ where评估JavaScript并且无法利用索引.
如何改进此查询?
Asy*_*sky 36
您想要的查询是这样的:
db.collection.find({"users":{"$not":{"$elemMatch":{"user":{$nin:[1,5,7]}}}}})
Run Code Online (Sandbox Code Playgroud)
这说明找到所有没有列表之外的元素的文档1,5,7.
Nei*_*unn 12
我不知道更好,但有几种不同的方法可以解决这个问题,并且取决于您可用的MongoDB版本.
不太确定这是否是你的意图,但是显示的查询将与第一个文档示例相匹配,因为在实现逻辑时,您将匹配该文档的数组中必须包含在示例数组中的元素.
因此,如果您确实希望文档包含所有这些元素,那么$all运算符将是显而易见的选择:
db.collection.find({ "users.user": { "$all": [ 1, 5, 7 ] } })
Run Code Online (Sandbox Code Playgroud)
但是假设您的逻辑实际上是有意的,至少根据建议您可以通过与$in操作员结合来"过滤"这些结果,以便减少您的逻辑$where**评估JavaScript中的条件:
db.collection.find({
"users.user": { "$in": [ 1, 5, 7 ] },
"$where": function() {
var ids = [1, 5, 7];
return this.users.every(function(u) {
return ids.indexOf(u.user) !== -1;
});
}
})
Run Code Online (Sandbox Code Playgroud)
虽然实际扫描的数据会乘以匹配文档中数组中的元素数量,但您得到的索引仍然比没有额外的过滤器更好.
或者甚至可能根据实际的数组条件考虑$and运算符的逻辑抽象$or以及可能的$size运算符:
db.collection.find({
"$or": [
{ "users.user": { "$all": [ 1, 5, 7 ] } },
{ "users.user": { "$all": [ 1, 5 ] } },
{ "users.user": { "$all": [ 1, 7 ] } },
{ "users": { "$size": 1 }, "users.user": 1 },
{ "users": { "$size": 1 }, "users.user": 5 },
{ "users": { "$size": 1 }, "users.user": 7 }
]
})
Run Code Online (Sandbox Code Playgroud)
因此,这是匹配条件的所有可能排列的几代,但性能可能会根据您可用的安装版本而有所不同.
注意:实际上在这种情况下完全失败,因为这会完全不同,实际上会产生逻辑$in
替换是使用聚合框架,由于您的集合中的文档数量,使用MongoDB 2.6及更高版本的一种方法,您的里程可能会因效率而异:
db.problem.aggregate([
// Match documents that "could" meet the conditions
{ "$match": {
"users.user": { "$in": [ 1, 5, 7 ] }
}},
// Keep your original document and a copy of the array
{ "$project": {
"_id": {
"_id": "$_id",
"date": "$date",
"users": "$users"
},
"users": 1,
}},
// Unwind the array copy
{ "$unwind": "$users" },
// Just keeping the "user" element value
{ "$group": {
"_id": "$_id",
"users": { "$push": "$users.user" }
}},
// Compare to see if all elements are a member of the desired match
{ "$project": {
"match": { "$setEquals": [
{ "$setIntersection": [ "$users", [ 1, 5, 7 ] ] },
"$users"
]}
}},
// Filter out any documents that did not match
{ "$match": { "match": true } },
// Return the original document form
{ "$project": {
"_id": "$_id._id",
"date": "$_id.date",
"users": "$_id.users"
}}
])
Run Code Online (Sandbox Code Playgroud)
因此,该方法使用一些新引入的集合运算符来比较内容,但当然您需要重组数组以进行比较.
正如所指出的,有一个直接的运算符来执行此$setIsSubset操作,其中在单个运算符中等效于上面的组合运算符:
db.collection.aggregate([
{ "$match": {
"users.user": { "$in": [ 1,5,7 ] }
}},
{ "$project": {
"_id": {
"_id": "$_id",
"date": "$date",
"users": "$users"
},
"users": 1,
}},
{ "$unwind": "$users" },
{ "$group": {
"_id": "$_id",
"users": { "$push": "$users.user" }
}},
{ "$project": {
"match": { "$setIsSubset": [ "$users", [ 1, 5, 7 ] ] }
}},
{ "$match": { "match": true } },
{ "$project": {
"_id": "$_id._id",
"date": "$_id.date",
"users": "$_id.users"
}}
])
Run Code Online (Sandbox Code Playgroud)
或者使用不同的方法,同时仍然利用$sizeMongoDB 2.6 中的运算符:
db.collection.aggregate([
// Match documents that "could" meet the conditions
{ "$match": {
"users.user": { "$in": [ 1, 5, 7 ] }
}},
// Keep your original document and a copy of the array
// and a note of it's current size
{ "$project": {
"_id": {
"_id": "$_id",
"date": "$date",
"users": "$users"
},
"users": 1,
"size": { "$size": "$users" }
}},
// Unwind the array copy
{ "$unwind": "$users" },
// Filter array contents that do not match
{ "$match": {
"users.user": { "$in": [ 1, 5, 7 ] }
}},
// Count the array elements that did match
{ "$group": {
"_id": "$_id",
"size": { "$first": "$size" },
"count": { "$sum": 1 }
}},
// Compare the original size to the matched count
{ "$project": {
"match": { "$eq": [ "$size", "$count" ] }
}},
// Filter out documents that were not the same
{ "$match": { "match": true } },
// Return the original document form
{ "$project": {
"_id": "$_id._id",
"date": "$_id.date",
"users": "$_id.users"
}}
])
Run Code Online (Sandbox Code Playgroud)
当然仍然可以做到这一点,尽管在2.6之前的版本中有点长篇大论:
db.collection.aggregate([
// Match documents that "could" meet the conditions
{ "$match": {
"users.user": { "$in": [ 1, 5, 7 ] }
}},
// Keep your original document and a copy of the array
{ "$project": {
"_id": {
"_id": "$_id",
"date": "$date",
"users": "$users"
},
"users": 1,
}},
// Unwind the array copy
{ "$unwind": "$users" },
// Group it back to get it's original size
{ "$group": {
"_id": "$_id",
"users": { "$push": "$users" },
"size": { "$sum": 1 }
}},
// Unwind the array copy again
{ "$unwind": "$users" },
// Filter array contents that do not match
{ "$match": {
"users.user": { "$in": [ 1, 5, 7 ] }
}},
// Count the array elements that did match
{ "$group": {
"_id": "$_id",
"size": { "$first": "$size" },
"count": { "$sum": 1 }
}},
// Compare the original size to the matched count
{ "$project": {
"match": { "$eq": [ "$size", "$count" ] }
}},
// Filter out documents that were not the same
{ "$match": { "match": true } },
// Return the original document form
{ "$project": {
"_id": "$_id._id",
"date": "$_id.date",
"users": "$_id.users"
}}
])
Run Code Online (Sandbox Code Playgroud)
这通常会以不同的方式完成,尝试一下,看看哪种方式最适合你.$in很可能与现有表格的简单组合可能是最好的.但在所有情况下,请确保您有一个可以选择的索引:
db.collection.ensureIndex({ "users.user": 1 })
Run Code Online (Sandbox Code Playgroud)
只要您以某种方式访问它,这将为您提供最佳性能,就像这里的所有示例一样.
我对此感到好奇,因此最终设计了一个测试案例,以便了解最佳性能.首先是一些测试数据生成:
var batch = [];
for ( var n = 1; n <= 10000; n++ ) {
var elements = Math.floor(Math.random(10)*10)+1;
var obj = { date: new Date(), users: [] };
for ( var x = 0; x < elements; x++ ) {
var user = Math.floor(Math.random(10)*10)+1,
group = Math.floor(Math.random(10)*10)+1;
obj.users.push({ user: user, group: group });
}
batch.push( obj );
if ( n % 500 == 0 ) {
db.problem.insert( batch );
batch = [];
}
}
Run Code Online (Sandbox Code Playgroud)
在一个集合中有10000个文档的长度为1..10的随机数组,随机值为1..0,我得到了430个文档的匹配数(从$in匹配中减少了7749 ),结果如下(平均):
$in子句的JavaScript :420ms$size:395ms$setIsSubset:250ms注意到除了最后两个以外的所有样品都具有大约100ms 的峰值方差,并且后两个都表现出220ms的响应.最大的变化是在JavaScript查询中,也显示结果慢100ms.
但这里的重点是相对于硬件,这在我的笔记本电脑下在VM下并不是特别好,但给出了一个想法.
因此,聚合,特别是具有集合运算符的MongoDB 2.6.1版本显然在性能上获胜,而$setIsSubset作为单个运算符的额外轻微增益.
这是特别有趣的(如2.4兼容方法所示)此过程中的最大成本将是$unwind声明(超过100ms avg),因此$in选择具有大约32ms的平均值,其余的流水线阶段执行时间小于100ms一般.因此,它提供了聚合与JavaScript性能的相对概念.