我不知道更好的方法,但有几种不同的方法可以解决这个问题,具体取决于您可用的 MongoDB 版本。
不太确定这是否是您的意图,但显示的查询将匹配第一个文档示例,因为在实现您的逻辑时,您正在匹配该文档数组中必须包含在示例数组中的元素。
因此,如果您确实希望文档包含这些元素的所有,那么$all 运算符将是显而易见的选择:
db.collection.find({ "users.user": { "$all": [ 1, 5, 7 ] } })
但是假设您的逻辑实际上是预期的,至少根据建议,您可以通过结合 $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;
});
}
})
虽然实际扫描的结果将乘以匹配文档中数组中的元素数量,但您会得到一个索引,但仍然比没有附加过滤器要好。
或者甚至可以考虑将$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"
}}
])
因此该方法使用一些新引入的set operators 来比较内容,当然您需要重组数组才能进行比较。
正如所指出的,$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 ) 与以下结果 (avg):
- 带有
$in 子句的 JavaScript:420 毫秒
- 与
$size 聚合:395 毫秒
- 与组数组计数聚合:650 毫秒
- 使用两个集合运算符聚合:275 毫秒
- 与
$setIsSubset 聚合:250ms
请注意,除了最后两个样本之外,所有样本的峰值方差都快了大约 100 毫秒,而最后两个样本都表现出 220 毫秒的响应。最大的变化出现在 JavaScript 查询中,它的结果也慢了 100 毫秒。
但这里的重点是相对于硬件而言,在我的笔记本电脑下的虚拟机上不是特别好,但给出了一个想法。
因此,集合运算符,特别是带有集合运算符的 MongoDB 2.6.1 版本显然在性能上胜出,而 $setIsSubset 作为单个运算符则带来了额外的小幅提升。
这特别有趣,因为(如 2.4 兼容方法所示)此过程中最大的成本将是 $unwind 语句(平均超过 100 毫秒),因此使用 @ 987654357@ 选择的平均时间约为 32 毫秒,其余流水线阶段的平均执行时间不到 100 毫秒。因此,这给出了聚合与 JavaScript 性能的相对概念。