我有一个文档集合:
date: Date
users: [
{ user: 1, group: 1 }
{ user: 5, group: 2 }
]
date: Date
users: [
{ user: 1, group: 1 }
{ user: 3, group: 2 }
]
我想对这个集合进行查询,以查找用户数组中的每个用户id都在另一个数组[1,5,7]中的所有文档。在这个例子中,只有第一个文档匹配。
我能找到的最好的解决方案是:
$where: function() {
var ids = [1, 5, 7];
return this.users.every(function(u) {
return ids.indexOf(u.user) !== -1;
});
}
不幸的是,这似乎会损害性能,在$where docs:
中声明$where计算JavaScript,不能利用索引。
我如何改进这个查询?
您想要的查询是:
db.collection.find({"users":{"$not":{"$elemMatch":{"user":{$nin:[1,5,7]}}}}})
查找所有元素不在列表1,5,7之外的文档
我不知道更好,但有几种不同的方法来解决这个问题,并取决于MongoDB的版本,你有可用的
不太确定这是否是您的意图,但是所示的查询将匹配第一个文档示例,因为在实现逻辑时,您正在匹配该文档数组中的元素,这些元素必须包含在示例数组中。
因此,如果您确实希望文档包含所有元素,那么$all
操作符将是显而易见的选择:
db.collection.find({ "users.user": { "$all": [ 1, 5, 7 ] } })
但是假设您的逻辑实际上是有意的,至少按照建议,您可以通过与 $in
运算符相结合来"过滤"这些结果,以便在评估的JavaScript中有更少的文档受制于 $where
**条件:
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;
});
}
})
您得到一个索引,虽然实际扫描的将乘以数组中来自匹配文档的元素数量,但仍然比没有额外的过滤器要好。
或者你甚至可以考虑 $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 }
]
})
所以这是匹配条件的所有可能排列的代,但是性能可能会根据可用的安装版本而变化。
注意:在这种情况下实际上是完全失败的,因为这做的事情完全不同,实际上导致逻辑 $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"
}}
])
因此,这种方法使用了一些新引入的集合操作符来比较内容,当然,为了进行比较,您需要重构数组。
如前所述,在$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"
}}
])
或者使用不同的方法,同时仍然利用MongoDB 2.6中的 $size
操作符:
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"
}}
])
当然仍然可以这样做,尽管在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"
}}
])
这通常是不同的方法,尝试一下,看看哪种最适合你。在所有可能的简单组合 $in
与您现有的形式可能是最好的。但在任何情况下,都要确保有一个可选择的索引:
db.collection.ensureIndex({ "users.user": 1 })
只要你以某种方式访问它,它就会给你最好的性能,就像这里所有的例子一样。
裁决
我对此很感兴趣,所以最终设计了一个测试用例来看看什么具有最好的性能。首先是一些测试数据生成:
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 = [];
}
}
集合中有10000个文档,随机数组从1..长度为10,包含随机值1..0时,我得到了430个文档的匹配计数(从 $in
match的7749个文档减少),结果如下(平均):
- JavaScript with
$in
clause: 420ms - 聚合
$size
: 395ms - 集合与组数组计数:650ms
- 两个集合运算符的聚合:275ms 与<<li>总strong>
$setIsSubset
: 250 ms 注意到,除了最后两个样本外,所有样本的峰值方差都快了大约100ms,最后两个都表现出220ms的响应。最大的变化是在JavaScript查询中,显示的结果也慢了100毫秒。
但这里的重点是相对于硬件而言的,在我的笔记本电脑上,在虚拟机下的硬件并不是特别好,但给了一个想法。
所以聚合,特别是MongoDB 2.6.1版本的集合操作符显然在性能上获胜,额外的增益来自于 $setIsSubset
作为单个操作符。
这是特别有趣的(如2.4兼容方法所示),该过程中的最大成本将是 $unwind
语句(平均超过100ms),因此 $in
选择的平均时间约为32ms,其余管道阶段的执行时间平均不到100ms。这就是聚合与JavaScript性能的关系
我刚刚花了一天的大部分时间尝试用对象比较而不是严格的相等来实现Asya上面的解决方案。所以我想在这里分享一下。
假设您将问题从userid扩展到完整用户。您希望找到users
数组中的每个项都出现在另一个用户数组中的所有文档:[{user: 1, group: 3}, {user: 2, group: 5},...]
这将不起作用:db.collection.find({"users":{"$not":{"$elemMatch":{"$nin":[{user: 1, group: 3},{user: 2, group: 5},...]}}}}})
,因为$nin只适用于严格相等。因此,我们需要为对象数组找到一种不同的表达"不在数组中"的方式。而使用$where
会大大降低查询速度。
解决方案:
db.collection.find({
"users": {
"$not": {
"$elemMatch": {
// if all of the OR-blocks are true, element is not in array
"$and": [{
// each OR-block == true if element != that user
"$or": [
"user": { "ne": 1 },
"group": { "ne": 3 }
]
}, {
"$or": [
"user": { "ne": 2 },
"group": { "ne": 5 }
]
}, {
// more users...
}]
}
}
}
})
使逻辑完整:$elemMatch匹配数组中没有用户的所有文档。因此$not将匹配数组中包含所有用户的所有文档