【问题标题】:Finding documents based on the minimum value in an array根据数组中的最小值查找文档
【发布时间】:2016-03-17 22:13:43
【问题描述】:

我的文档结构类似于:

    {
        _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 介于某个限制之间的所有文档。

我可以先对选项进行放松(然后取最小值),然后运行比较查询,但我是 mongo 新手,不确定性能如何 受展开操作的影响。这种类型的文件数量大约为几百万。

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

【问题讨论】:

  • 那么您正在寻找的实际情况是什么?它是最小options.value 小于和大于一个范围的一组数字吗?你有哪个 MongoDB 版本?最新的 3.2.x 版本系列具有在没有 $unwind 的情况下允许这样做的功能。
  • 谢谢!你指的是 $elemMatch 吗?然后我使用比较查询运算符 ..something like db.survey.find( { options: { $elemMatch: { value: { $gte: 8 }, /*any other filters*/} } } )
  • 不,我不是。不只是无论如何。你有哪个 MongoDB 版本?
  • 我有最新的 3.2

标签: mongodb mongodb-query aggregation-framework


【解决方案1】:

使用$unwind 确实非常昂贵,尤其是对于较大的数组,但在所有使用情况下都有成本。在没有真正的结构变化的情况下,有几种方法可以在此处不需要 $unwind

纯聚合

在基本情况下,从 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 内部完成,因为我们想在逻辑条件下“两次”使用结果。帮助我们不要重复自己)是首先提取@来自"options" 数组的 987654338@ 数据。这是使用$map 完成的。

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

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

在这种情况下,条件是 $and,其中“两种”$gte$lt 的逻辑形式针对计算值返回 true(从 $let"$$min_value")。

然后,$redact 阶段具有特殊参数,当条件为true$$PRUNE 时从结果文档为false 时应用于$$KEEP 文档。

这很像$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 的新功能也可以在没有字段的情况下使用,如果您更喜欢动态条件。

【讨论】:

  • 非常感谢您的详细解释。学到了很多新东西!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-01-13
  • 2010-10-25
  • 2018-09-24
  • 2021-12-31
  • 1970-01-01
相关资源
最近更新 更多