MongoDB - 聚合框架(总数统计)

13

在MongoDB上正常运行“查找”查询时,可以对返回的游标执行“count”操作以获取总结果数(无论限制是什么)。因此,即使将结果集限制为10(例如),我仍然可以知道结果的总数是53(例如)。

但是,如果我正确理解的话,聚合框架不会返回游标,而只返回结果。所以,如果我使用了$limit管道操作符,无论该值如何限制,我如何知道结果的总数呢?

我想我可以两次运行聚合操作(一次通过$group计算结果总数,一次通过$limit获得实际受限结果),但这似乎效率低下。

另一种方法是在$limit操作之前通过$group将结果总数附加到文档中,但这似乎也效率低下,因为这个数字将附加到每个文档上(而不是仅仅对于一组返回一次)。

我是否遗漏了什么?有什么想法吗?谢谢!

例如,如果这是查询:

db.article.aggregate(
    { $group : {
        _id : "$author",
        posts : { $sum : 1 }
    }},
    { $sort : { posts: -1 } },
    { $limit : 5 }
);

$limit之前,我如何知道有多少结果可用?结果不是光标,因此我不能只运行计数。


如果您将您的查询作为问题示例附加到问题中,这将非常有帮助。 - Roman Pekar
添加了一个代码示例。不过,问题是通用的(我想)。谢谢! - Assaf Hershko
重复的问题,较好的答案在MongoDB聚合:如何获取总记录数?中。 - Dan Dascalescu
7个回答

3
有一个使用push和slice的解决方案:https://dev59.com/vGIj5IYBdhLWcg3wWj-d#39784851(@emaniacs也在这里提到了它)。
但我更喜欢使用两个查询。使用$$ROOT推送并使用$slice的解决方案会遇到16MB文档内存限制的问题,适用于大型集合。此外,对于大型集合,两个查询一起似乎比使用$$ROOT推送的查询更快。您也可以并行运行它们,因此只受限于两个查询中较慢的一个(可能是排序查询)。
  1. 首先进行过滤,然后按ID分组以获取过滤元素的数量。不要在此处过滤,这是不必要的。
  2. 第二个查询用于过滤、排序和分页。
我已经采用了这个使用两个查询和聚合框架的解决方案(注意-我在这个示例中使用node.js)。
var aggregation = [
  {
    // If you can match fields at the begining, match as many as early as possible.
    $match: {...}
  },
  {
    // Projection.
    $project: {...}
  },
  {
    // Some things you can match only after projection or grouping, so do it now.
    $match: {...}
  }
];


// Copy filtering elements from the pipeline - this is the same for both counting number of fileter elements and for pagination queries.
var aggregationPaginated = aggregation.slice(0);

// Count filtered elements.
aggregation.push(
  {
    $group: {
      _id: null,
      count: { $sum: 1 }
    }
  }
);

// Sort in pagination query.
aggregationPaginated.push(
  {
    $sort: sorting
  }
);

// Paginate.
aggregationPaginated.push(
  {
    $limit: skip + length
  },
  {
    $skip: skip
  }
);

// I use mongoose.

// Get total count.
model.count(function(errCount, totalCount) {
  // Count filtered.
  model.aggregate(aggregation)
  .allowDiskUse(true)
  .exec(
  function(errFind, documents) {
    if (errFind) {
      // Errors.
      res.status(503);
      return res.json({
        'success': false,
        'response': 'err_counting'
      });
    }
    else {
      // Number of filtered elements.
      var numFiltered = documents[0].count;

      // Filter, sort and pagiante.
      model.request.aggregate(aggregationPaginated)
      .allowDiskUse(true)
      .exec(
        function(errFindP, documentsP) {
          if (errFindP) {
            // Errors.
            res.status(503);
            return res.json({
              'success': false,
              'response': 'err_pagination'
            });
          }
          else {
            return res.json({
              'success': true,
              'recordsTotal': totalCount,
              'recordsFiltered': numFiltered,
              'response': documentsP
            });
          }
      });
    }
  });
});

2
Assaf,未来会对聚合框架进行一些增强,这可能会使您能够轻松地在一次计算中完成您的计算,但现在,最好通过并行运行两个查询来执行您的计算:一个用于聚合您的顶级作者的帖子数量,另一个聚合则计算所有作者的总帖子数。此外,请注意,如果您只需要对文档进行计数,则使用计数函数是非常高效的计算方法。MongoDB将计数缓存到btree索引中,从而允许在查询上快速计数。
如果这些聚合计算变得缓慢,有几种策略可以采用。首先,请记住,如果适用,您要以$match开始查询以减少结果集。 $matches 也可以通过索引加速。其次,您可以将这些计算作为预聚合。不要每次用户访问应用程序的某个部分时都可能运行这些聚合计算,而是定期在后台运行聚合计算,并将聚合值存储在包含预聚合值的集合中。这样,您的页面只需从该集合中查询预先计算的值即可。

谢谢回答。很有用的信息。在我的真实应用程序中,采用了多种解决方案的组合,例如在可能的情况下使用$match,在可能的情况下进行预计算,并在其他情况下不进行计数。上面的查询只是一个示例(因为我被要求提供代码)。 - Assaf Hershko
3
@Dylan,你知道这些改进是否已经完成了吗? - Dan

1

如果您不想同时运行两个查询(一个用于聚合顶级作者的#posts,另一个用于计算所有作者的总帖子数),则可以在管道上删除$limit,并在结果上使用

totalCount = results.length;
results.slice(number of skip,number of skip + number of limit);

例如:

db.article.aggregate([
    { $group : {
        _id : "$author",
        posts : { $sum : 1 }
    }},
    { $sort : { posts: -1 } }
    //{$skip : yourSkip},    //--remove this
    //{ $limit : yourLimit }, // remove this too
]).exec(function(err, results){
  var totalCount = results.length;//--GEt total count here
   results.slice(yourSkip,yourSkip+yourLimit);
});

1

$facets聚合操作可以用于Mongo版本>= 3.4。 这允许在管道的特定阶段分叉为多个子管道,允许在这种情况下构建一个子管道来计算文档数量,另一个子管道用于排序、跳过、限制。

这样可以避免在多个请求中多次执行相同的阶段。


0

我遇到了同样的问题,通过使用$project$slice$$ROOT解决了。

db.article.aggregate(
{ $group : {
    _id : '$author',
    posts : { $sum : 1 },
    articles: {$push: '$$ROOT'},
}},
{ $sort : { posts: -1 } },
{ $project: {total: '$posts', articles: {$slice: ['$articles', from, to]}},
).toArray(function(err, result){
    var articles = result[0].articles;
    var total = result[0].total;
});

你需要声明 fromto 变量。

https://docs.mongodb.com/manual/reference/operator/aggregation/slice/


-1
在我的情况下,我们使用 $out 阶段将聚合结果集转储到一个临时/缓存表中,然后对其进行计数。由于我们需要对结果进行排序和分页,因此我们在临时表上添加索引,并将表名保存在会话中,在会话关闭/缓存超时时删除该表。

引入$facet听起来有些过度 - 可以参考这个更好的问题的答案,MongoDB聚合:如何获取总记录数? - Dan Dascalescu

-1

我可以使用 aggregate().toArray().length 获取总数。


这不可扩展。 - Dan Dascalescu

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