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

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.

  • 关键是$ elemMatch,它区分您希望特定元素满足特定条件,而不是满足条件的整个文档.因为数组允许"users.user"在单个文档中具有多个值,所以无论您是指任何元素还是特定元素,它都可能不明确.正如你所拥有的那样,任何元素都可以满足$中的一个而不是其中之一,它就相当于一个$ in.$ elemMatch说单个元素必须不是其中之一,这意味着现在必须有另一个不是1,5或7的元素.$ not现在不包括那些_documents_ (4认同)
  • 好答案.但值得注意的是,这还将包括"用户"丢失或空的文档. (4认同)
  • ps 这个答案在从另一个“答案”生成的样本数据集上需要 10 毫秒 (3认同)
  • 太棒了,这似乎给了我与我问题中的查询相同的结果,并且它的返回速度提高了大约 10 倍。 (2认同)

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
  • 具有组数组的聚合:650ms
  • 与两个集合运算符聚合:275ms
  • 聚合$setIsSubset:250ms

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

但这里的重点是相对于硬件,这在我的笔记本电脑下在VM下并不是特别好,但给出了一个想法.

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

这是特别有趣的(如2.4兼容方法所示)此过程中的最大成本将是$unwind声明(超过100ms avg),因此$in选择具有大约32ms的平均值,其余的流水线阶段执行时间小于100ms一般.因此,它提供了聚合与JavaScript性能的相对概念.

  • @AsyaKamsky嗯,你是对的,尽管否定了一个索引,这将是更好的解决方案.但没有必要像你回答的那样粗鲁. (4认同)