如何在 mongodb 中聚合第一个或最后一个时仅选择非空值



我的数据代表一个字典,它接收一堆更新和潜在的新字段(元数据被添加到帖子中)。所以像这样:

> db.collection.find()
{ _id: ..., 'A': 'apple', 'B': 'banana' },
{ _id: ..., 'A': 'artichoke' },
{ _id: ..., 'B': 'blueberry' },
{ _id: ..., 'C': 'cranberry' }

挑战 - 我想找到忽略空白值的每个键的第一个(或最后一个)值(即我想要某种在字段而不是文档级别工作的条件组)。(相当于更新后元数据的起始或结束版本)。

问题是:

db.collection.aggregate([
  { $group: {
    _id: null,
    A: { $last: '$A' },
    B: { $last: '$B' }, 
    C: { $last: '$C' }
  }}
])

用 null 填充空白(而不是在结果中跳过它们),所以我得到:

{ '_id': ..., 'A': null, 'B': null, 'C': 'cranberry' }

当我想要时:

{ '_id': ..., 'A': 'artichoke', 'B': 'blueberry', 'C': cranberry' }

我不认为这是你真正想要的,但它确实解决了你提出的问题。聚合框架无法真正做到这一点,因为您正在请求来自不同文档的不同列的"最终结果"。实际上只有一种方法可以做到这一点,而且非常疯狂:

db.collection.aggregate([
    { "$group": {
        "_id": null,
        "A": { "$push": "$A" },
        "B": { "$push": "$B" },
        "C": { "$push": "$C" }
    }},
    { "$unwind": "$A" },
    { "$group": {
        "_id": null,
        "A": { "$last": "$A" },
        "B": { "$last": "$B" },
        "C": { "$last": "$C" }
    }},
    { "$unwind": "$B" },
    { "$group": {
        "_id": null,
        "A": { "$last": "$A" },
        "B": { "$last": "$B" },
        "C": { "$last": "$C" }
    }},
    { "$unwind": "$C" },
    { "$group": {
        "_id": null,
        "A": { "$last": "$A" },
        "B": { "$last": "$B" },
        "C": { "$last": "$C" }
    }},
])

本质上,您将压缩文档,将所有找到的元素推送到数组中。然后解开每个数组,并从那里取出$last元素。您需要为每个字段执行此操作,以便获取每个数组的最后一个元素,这是该字段的最后一个匹配项。

不是真正的好,肯定会在任何有意义的集合上爆炸BSON 16MB的限制。

因此,您真正想要的是为每个字段寻找"上次看到"的值。您可以通过迭代集合并保留未null的值来暴力执行此操作。你甚至可以在服务器上像这样使用 mapReduce 来执行此操作:

db.collection.mapReduce(
   function () {
      if (start == 0)
        emit( 1, "A" );
      start++;
      current = this;
      Object.keys(store).forEach(function(key) {
        if ( current.hasOwnProperty(key) )
          store[key] = current[key];
      });
    },
    function(){},
    {
        "scope": { "start": 0, "store": { "A": null, "B": null, "C": null } },
        "finalize": function(){ return store },
        "out": { "inline": 1 }
    }
)

这也行得通,但迭代整个集合几乎和将所有内容与聚合混合在一起一样糟糕。

在这种情况下,您真正想要的是三个查询,理想情况下是并行的,以便仅获取每个属性最后看到的谨慎值:

> db.collection.find({ "A": { "$exists": true } }).sort({ "$natural": -1 }).limit(1)
{ "_id" : ObjectId("54b319cd6997a054ce4d71e7"), "A" : "artichoke" }
> db.collection.find({ "B": { "$exists": true } }).sort({ "$natural": -1 }).limit(1)
{ "_id" : ObjectId("54b319cd6997a054ce4d71e8"), "B" : "blueberry" }
> db.collection.find({ "C": { "$exists": true } }).sort({ "$natural": -1 }).limit(1)
{ "_id" : ObjectId("54b319cd6997a054ce4d71e9"), "C" : "cranberry" }

从字面上看,更好的是在每个属性上创建一个稀疏索引,并通过$gt和空白字符串进行查询。这可确保使用索引,并且作为稀疏索引,它将仅包含存在该属性的文档。您需要.hint()这一点,但您仍然需要排序$natural排序:

db.collection.ensureIndex({ "A": -1 },{ "sparse": 1 })
db.collection.ensureIndex({ "B": -1 },{ "sparse": 1 })
db.collection.ensureIndex({ "C": -1 },{ "sparse": 1 })

> db.collection.find({ "A": { "$gt": "" } }).hint({ "A": -1 }).sort({ "$natural": -1 }).limit(1)
{ "_id" : ObjectId("54b319cd6997a054ce4d71e7"), "A" : "artichoke" }
> db.collection.find({ "B": { "$gt": "" } }).hint({ "B": -1 }).sort({ "$natural": -1 }).limit(1)
{ "_id" : ObjectId("54b319cd6997a054ce4d71e8"), "B" : "blueberry" }
> db.collection.find({ "C": { "$gt": "" } }).hint({ "C": -1 }).sort({ "$natural": -1 }).limit(1)
{ "_id" : ObjectId("54b319cd6997a054ce4d71e9"), "C" : "cranberry" }

这是解决你在这里所说的问题的最佳方法。但正如我所说,这就是你认为你需要解决它的方式。您的真正问题可能还有另一种方法来存储和查询。

Mongo 3.6 开始,对于那些使用 $first$last 作为从分组记录(不一定是实际的第一个或最后一个)中获取一个值的方法,$group$mergeObjects可以用作从分组项目中查找非空值的方法:

// { "A" : "apple", "B" : "banana" }
// { "A" : "artichoke" }
// { "B" : "blueberry" }
// { "C" : "cranberry" }
db.collection.aggregate([
  { $group: {
      _id: null,
      A: { $mergeObjects: { a: "$A" } },
      B: { $mergeObjects: { b: "$B" } },
      C: { $mergeObjects: { c: "$C" } }
  }}
])
// { _id: null, A: { a: "artichoke" }, B: { b: "blueberry" }, C: { c: "cranberry" } }

$mergeObjects基于每个分组记录累积一个对象。需要注意的是,$mergeObjects将合并未null的优先级值。但这需要将累积字段修改为对象,因此"尴尬"{ a: "$A" }

如果输出格式不完全符合您的预期,则始终可以使用额外的$project阶段。

所以我刚刚考虑了如何回答这个问题,但有兴趣听听人们对这有多对/错的看法。根据@NeilLunn的回复,我想我会达到 BSON 限制,使他的版本更适合提取数据,但对我的应用程序来说,我可以一次性运行此查询很重要。(也许我真正的问题是数据设计)。

我们

遇到的问题是,在"分组依据"中,我们为每个文档拉入一个版本的 A、B、C。所以我的解决方案是通过更改(稍微)原始数据结构来告诉聚合它应该拉入哪些字段,以告诉引擎每个文档中有哪些键:

> db.collection.find()
{ _id: ..., 'A': 'apple', 'B': 'banana', 'Keys': ['A', 'B']},
{ _id: ..., 'A': 'artichoke', 'Keys': ['A']},
{ _id: ..., 'B': 'blueberry', 'Keys': ['B']},
{ _id: ..., 'C': 'cranberry', 'Keys': ['C']}

现在我们可以$unwind 'Keys',然后'Keys'分组为'_id' .因此:

db.collection.aggregate([
                          {'$unwind': 'Keys'}, 
                          {'$group': 
                             {'_id': 'Keys', 
                                'A': {'$last': '$A'}, 
                                'B': {'$last': '$B'}, 
                                'C': {'$last': '$C'}
                             }
                           }
                        ])

我得到一系列_id等于键的文档:

{_id: 'A', 'A': 'artichoke', 'B': null, 'C': null}, 
{_id: 'B', 'A': null, 'B': 'blueberry', 'C': null}, 
{_id: 'C', 'A': null, 'B': null, 'C': 'cranberry'}

然后,您可以提取所需的结果,因为您知道键X的值仅对X_id的结果有效。

(当然下一个问题是如何将这一系列文档缩减为一个,每次都采取适当的字段)

相关内容

  • 没有找到相关文章

最新更新