根据数组中的最小值查找文档

时间:2016-03-17 22:13:44

标签: mongodb mongodb-query aggregation-framework

我的文档结构类似于:

    {
        _id: ...,
        key1: ....
        key2: ....
        ....
        min_value: //should be the minimum of all the values in options
        options: [
        {
            source: 'a',
            value: 12,

        },
        {
            source: 'b',
            value: 10,
        },
        ...
        ]
    },
    {
        _id: ...,
        key1: ....
        key2: ....
        ....
        min_value: //should be the minimum of all the values in options
        options: [
        {
            source: 'a',
            value: 24,

        },
        {
            source: 'b',
            value: 36,
        },
        ...
        ]
    }

期权中各种来源的价值将不断更新(几分钟或几小时), 假设选项数组的大小没有改变,即没有额外的元素添加到列表

我的查询属于以下类型:

- 查找所有选项的min_value落在某个限制之间的所有文档。

我可以先对选项进行解除(然后取min),然后运行比较查询,但我是mongo的新手并且不确定性能如何 放松操作会影响。这类文件的数量约为几百万。

或者有没有人有任何改变文档结构的建议,这可以帮助我简化这个查询? (除了为每个源创建单独的文档 - 它将涉及大量数据重复) 谢谢!

1 个答案:

答案 0 :(得分:2)

使用$unwind确实相当昂贵,最值得注意的是使用更大的阵列,但在所有使用情况下都会有成本。在没有真正的结构变化的情况下,有两种方法可以在这里不需要$unwind

Pure Aggregation

在基本情况下,从MongoDB 3.2.x版本系列开始,$min运算符除了标准的分组累加器角色外,还可以在“投影”意义上直接处理值数组。这意味着在相关$map运算符的帮助下处理数组的元素,然后您可以在不使用$unwind的情况下获取最小值:

db.collection.aggregate([
    // Still makes sense to use an index to select only possible documents
    { "$match": {
        "options": { 
            "$elemMatch": {
                "value": { "$gte": minValue, "$lt": maxValue }
            }
        }
    }},

    // Provides a logical filter to remove non-matching documents
    { "$redact": {
        "$cond": {
            "if": {
                "$let": {
                    "vars": {
                        "min_value": {
                            "$min": {
                                "$map": {
                                    "input": "$options",
                                    "as": "option",
                                    "in": "$$option.value"
                                }
                            }
                        }
                    },
                    "in": { "$and": [
                        { "$gte": [ "$$min_value", minValue ] },
                        { "$lt": [ "$$min_value", maxValue ] }
                    ]}
                }
            },
            "then": "$$KEEP",
            "else": "$$PRUNE"
        }
    }},

    // Optionally return the min_value as a field
    { "$project": {
        "min_value": { 
            "$min": {
                "$map": {
                    "input": "$options",
                    "as": "option",
                    "in": "$$option.value"
                }
            }
        }
    }}
])

基本情况是从数组中获取“最小”值(在$let内完成,因为我们想在逻辑条件中使用“两次”结果。帮助我们不重复自己)是先提取来自"value"数组的"options"数据。这是使用$map完成的。

$map的输出是一个只包含这些值的数组,因此它作为$min的参数提供,然后返回该数组的最小值。

使用$redact有点像$match管道阶段,不同之处在于,不是需要在正在检查的文档中“存在”字段,而是仅通过计算形成逻辑条件

在这种情况下,条件为$and$gte$lt的逻辑形式的“两者”对计算值(从$let返回{{1} }})。

"$$min_value"阶段具有特殊参数,当条件为$redact$$KEEP文件来自true时,$$PRUNE文件适用false }}

这非常类似于在$project然后$match实际将值投影到文档中,然后在另一个阶段进行过滤,但所有这些都在一个阶段完成。当然,您可能希望在返回的内容中$project得到结果字段,但如果您使用$redact“首先”删除不匹配的文档,通常会减少工作量。

更新文件

当然,我认为最佳选项是将"min_value"字段实际保留在文档中,而不是在运行时进行处理。因此,在更新期间添加或更改数组项时,这是一件非常简单的事情。

为此,有$min“更新”运算符。与$push追加时使用它:

db.collection.update({
    { "_id": id },
    {
        "$push": { "options": { "source": "a", "value": 9 } },
        "$min": { "min_value": 9 }
    }
})

或更新元素的值时:

db.collection.update({
    { "_id": id, "options.source": "a" },
    {
        "$set": { "options.$.value": 9 },
        "$min": { "min_value": 9 }
    }
})

如果文档中的当前"min_value"大于$min中的参数,或者该密钥尚不存在,则将写入给定的值。如果它大于,则现有值保持不变,因为它已经是较小的值。

您甚至可以使用简单的“批量”操作更新来设置所有现有数据:

var ops = [];

db.collection.find({ "min_value": { "$exists": false } }).forEach(function(doc) {
    // Queue operations
    ops.push({
        "updateOne": {
           "filter": { "_id": doc._id },
           "update": {
               "$min": {
                   "min_value": Math.min.apply(
                       null,
                       doc.options.map(function(option) {
                           return option.value
                       })
                   )
               }
           }
        }
    });

    // Write once in 1000 documents
    if ( ops.length == 1000 ) {
        db.collection.bulkWrite(ops);
        ops = [];
    }
});

// Clear any remaining operations
if ( ops.length > 0 )
    db.collection.bulkWrite(ops);

然后使用一个字段,它只是一个简单的范围选择:

db.collection.find({
    "min_value": {
        "$gte": minValue, "$lt": maxValue
    }
})

因此,在文档中保留一个字段(或者如果您经常需要不同条件的字段)确实应该符合您的最佳利益,因为这样可以提供最有效的查询。

当然,聚合$min$map的新功能也可以在没有字段的情况下使用,如果您更喜欢更动态的条件。