检查数组中的每个元素是否与条件匹配

时间:2014-05-11 16:21:31

标签: mongodb mapreduce 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 }
]

我想查询这个集合,找到我的用户数组中每个用户id都在另一个数组中的所有文档,[1,5,7]。在此示例中,只有第一个文档匹配。

我能找到的最佳解决方案是:

$where: function() { 
  var ids = [1, 5, 7];
  return this.users.every(function(u) { 
    return ids.indexOf(u.user) !== -1;
  });
}

不幸的是,这似乎损害了$where文档中的性能:

  

$ where评估JavaScript并且无法利用索引。

如何改进此查询?

3 个答案:

答案 0 :(得分:33)

您想要的查询是:

db.collection.find({"users":{"$not":{"$elemMatch":{"user":{$nin:[1,5,7]}}}}})

这说明找到所有没有列表之外的元素的文档1,5,7。

答案 1 :(得分:11)

我不知道更好,但有几种不同的方法可以解决这个问题,具体取决于您可以使用的MongoDB版本。

不太确定这是否是您的意图,但显示的查询将与第一个文档示例匹配,因为在您实现逻辑时,您将匹配该文档的数组中必须包含在示例数组中的元素。 / p>

因此,如果您确实希望文档包含所有这些元素,那么$all运算符将是显而易见的选择:

db.collection.find({ "users.user": { "$all": [ 1, 5, 7 ] } })

但是假设您的逻辑实际上是有意的,至少根据建议,您可以通过与$in运算符组合来“过滤”这些结果,以便更少的文档受 {的约束评估的JavaScript中的{3}} 条件:

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;
        });
    }
})

虽然实际扫描的数量会乘以匹配文档中数组中元素的数量,但您得到的索引仍然比没有额外的过滤器更好。

或者甚至可能考虑$where**运算符与$and$or运算符结合使用的逻辑抽象,具体取决于您的实际数组条件:

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"
    }}
])

因此,为了比较内容,该方法使用了一些新引入的$size,但当然您需要重新构建数组才能进行比较。

正如所指出的,有一个直接的运算符在set operators中执行此操作,它在单个运算符中执行上述组合运算符的等效操作:

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中的$setIsSubset运算符:

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"
    }}
])

这通常会以不同的方式完成,尝试一下,看看什么最适合你。很有可能$size与现有形式的简单组合可能是最好的组合。但在所有情况下,请确保您有一个可以选择的索引:

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个文档的匹配计数(从 {{1}减少到7749 } 匹配)与以下结果(平均):

  • 包含 $in 子句的JavaScript:420ms
  • $in 汇总:395毫秒
  • 与群组数量聚合:650毫秒
  • 使用两个集合运算符进行聚合:275ms
  • $size 汇总: 250ms

注意到除了最后两个样本之外的所有样本都具有大约100ms的峰值方差,并且后两个都表现出220ms的响应。最大的变化是在JavaScript查询中,它也显示出慢100ms的结果。

但这里的要点与硬件相关,在我的笔记本电脑下,在VM下并不是特别好,但是给出了一个想法。

因此,聚合,特别是具有集合运算符的MongoDB 2.6.1版本显然在性能上获胜,而 $setIsSubset 作为单个运算符获得额外的轻微增益。

这是特别有趣的(如2.4兼容方法所示)此过程中最大的成本是 $setIsSubset 语句(超过100毫秒平均值),所以使用 $unwind 选择具有大约32ms的平均值,其余的流水线阶段平均执行时间不到100ms。因此,这给出了聚合与JavaScript性能的相对概念。

答案 2 :(得分:0)

我只花了大量时间尝试使用对象比较而不是严格的平等来实现上面的Asya解决方案。所以我想我会在这里分享。

我们假设您将问题从userIds扩展为完整用户。 您希望找到其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只适用于严格的平等。所以我们需要找到一种不同的表达方式&#34; Not in array&#34;对于对象数组。使用$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将匹配包含阵列中所有用户的所有文档。