从每个组中选择前N行

22

我在我的博客平台中使用了MongoDB,用户可以创建自己的博客。所有博客的所有条目都在一个entries集合中。一个条目的文档如下:

{
  'blog_id':xxx,
  'timestamp':xxx,
  'title':xxx,
  'content':xxx
}

就像问题所说的那样,有没有什么方法可以选择每个博客的最后3个条目?

5个回答

32
你需要首先按照文集中的blog_idtimestamp字段进行排序,然后进行初始分组,创建一个按降序排列的原始文档数组。之后,你可以使用该文档数组进行切片操作以返回前3个元素。
可以通过以下示例理解这种方法的直觉:
db.entries.aggregate([
    { '$sort': { 'blog_id': 1, 'timestamp': -1 } }, 
    {       
        '$group': {
            '_id': '$blog_id',
            'docs': { '$push': '$$ROOT' },
        }
    },
    {
        '$project': {
            'top_three': { 
                '$slice': ['$docs', 3]
            }
        }
    }
])

自从聚合函数可用以后,这就是现在更好的答案。 - Calin Pirtea
3
如果每个组有几千个文档,我猜组阶段将把它们全部保存在“docs”数组中,而我们只想要最后3个文档,并且不需要保留其他任何内容。你知道在Mongo 4.2中是否有一种更有效的方法(最多只保留3个文档),吗?(我猜在4.4中,您可以使用自定义累加器函数。) - Qtax

3

Mongo 5.2 开始,这是一个完美的使用案例,可以使用新的$topN 聚合累加器:

// { blog_id: "a", title: "plop",  content: "smthg" }
// { blog_id: "b", title: "hum",   content: "meh"   }
// { blog_id: "a", title: "hello", content: "world" }
// { blog_id: "a", title: "what",  content: "ever"  }
db.collection.aggregate([
  { $group: {
    _id: "$blog_id",
    messages: { $topN: { n: 2, sortBy: { _id: -1 }, output: "$$ROOT" } }
  }}
])
// {
//   _id: "a",
//   messages: [
//     { blog_id: "a", title: "what",  content: "ever" },
//     { blog_id: "a", title: "hello", content: "world" }
//   ]
// }
// {
//   _id: "b",
//   messages: [
//     { blog_id: "b", title: "hum", content: "meh" }
//   ]
// }

该操作应用了一个$topN组累加,它:

  • 对于每个组,取前2个元素(n: 2
  • 前2个元素是由sortBy: {_id: -1}定义的,这意味着按照插入顺序的相反顺序。
  • 对于每个记录,在组的列表中推送整个记录 (output: "$$ROOT"),因为$$ROOT代表正在处理的整个文档。

1

在基本的MongoDB中,如果您可以接受以下两点,则可以实现此目标:

  • 在您的条目文档中添加一个额外的字段,我们将其称为“age”。
  • 执行一个新的博客条目,需要进行额外的更新。

如果可以,请按照以下步骤操作:

  1. 创建新介绍时,执行正常的插入,然后执行此更新以增加所有帖子(包括您刚刚为此博客插入的帖子)的年龄:

    db.entries.update({blog_id: BLOG_ID}, {age:{$inc:1}}, false, true)

  2. 在查询时,请使用以下查询语句,它将返回每个博客最近的3个条目:

    db.entries.find({age:{$lte:3}, timestamp:{$gte:STARTOFMONTH, $lt:ENDOFMONTH}}).sort({blog_id:1, age:1})

请注意,这种解决方案实际上是并发安全的(没有具有重复年龄的条目)。


明白了,我没有考虑到这种情况。在创建新帖子时进行额外更新不会成为问题。但是,当用户删除帖子时,我们必须更新所有其他帖子的“年龄”字段。但这个更新只需要在被删除的帖子的“年龄”<= 3时才会发生。我有遗漏什么吗? - Tacaza
是的,你不应该将那个更新限制在年龄小于3岁的条件上,因为这样会导致年龄重复。原地更新非常快,所以这不应该是问题。删除意味着删除该条目并将年龄减1,其中年龄 > 被删除帖子的年龄。祝好运。 - Remon van Vliet
对于记录数量较少且更新不频繁的情况下,使用它是很好的选择。但如果我需要从两个用户之间的每个对话中获取最后一条消息,并且有成千上万条消息每分钟都在增加,那么将其用于消息系统是否有效呢?我认为每次更新数千条消息的“年龄”并不是一种有效的方法。您能否就这种情况给出一些建议? - Roman
@oyatek 这有点取决于您的确切用例和读写比率。如果您打开一个带有您具体问题的问题,我会看一下。 - Remon van Vliet
是的,问题在这里 - http://stackoverflow.com/questions/9859713/mongodb-get-1-last-message-from-each-conversation - (我已经标记为已回答,但我会感激您的回答) - Roman

0

这个答案使用了drcosta从另一个问题中的map reduce技术,解决了问题。

在MongoDB中,如何使用map reduce技术来实现按最近时间排序的分组操作

mapper = function () {
  emit(this.category, {top:[this.score]});
}

reducer = function (key, values) {
  var scores = [];
  values.forEach(
    function (obj) {
      obj.top.forEach(
        function (score) {
          scores[scores.length] = score;
      });
  });
  scores.sort();
  scores.reverse();
  return {top:scores.slice(0, 3)};
}

function find_top_scores(categories) {
  var query = [];
  db.top_foos.find({_id:{$in:categories}}).forEach(
    function (topscores) {
      query[query.length] = {
        category:topscores._id,
        score:{$in:topscores.value.top}
      };
  });
  return db.foo.find({$or:query});

0

使用分组(聚合)是可能的,但这将创建一个完整的表扫描。

你真的需要恰好3个吗?或者你可以设置一个限制...例如:从上周/上个月最多获取3篇文章?


理想情况下,我想精确地选择最近的3篇文章,但如果我无法找到除数据非规范化以外的解决方案,那么选择上个月的最多3篇文章也足够了。您能否给我一个示例,说明如何实现这一目标?在我阅读过的所有mongodb映射/还原教程中,他们只展示如何计算统计信息(汇总)... - Tacaza

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接