【问题标题】:MongoDB - Aggregation Framework (Total Count)MongoDB - 聚合框架(总数)
【发布时间】:2013-07-20 00:11:06
【问题描述】:

在 MongoDB 上运行正常的“查找”查询时,我可以通过在返回的游标上运行“计数”来获得总结果计数(不考虑限制)。因此,即使我将结果集限制为 10(例如),我仍然可以知道结果总数为 53(再次,例如)。

如果我理解正确,那么聚合框架不会返回游标,而只是返回结果。因此,如果我使用$limit 管道运算符,我如何知道结果的总数而不管所述限制?

我想我可以运行两次聚合(一次通过$group 计算结果,一次通过$limit 计算实际有限的结果),但这似乎效率低下。

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

我在这里遗漏了什么吗?有任何想法吗?谢谢!

例如,如果这是查询:

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

我怎么知道有多少结果可用($limit 之前)?结果不是游标,所以我不能只对它进行计数。

【问题讨论】:

标签: mongodb mongodb-query


【解决方案1】:

有一个使用 push 和 slice 的解决方案:https://stackoverflow.com/a/39784851/4752635(@emaniacs 在这里也提到过)。

但我更喜欢使用 2 个查询。推送 $$ROOT 并使用 $slice 的解决方案遇到了 16MB 的大型集合的文档内存限制。此外,对于大型集合,两个查询一起运行似乎比使用 $$ROOT 推送的查询运行得更快。您也可以并行运行它们,因此您只会受到两个查询中较慢的查询(可能是排序的那个)的限制。

  1. 首先进行过滤,然后按 ID 分组以获取过滤元素的数量。不要在这里过滤,没有必要。
  2. 过滤、排序和分页的第二个查询。

我已经使用 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,在不久的将来会对聚合框架进行一些增强,这可能使您可以轻松地一次完成计算,但现在,最好通过并行运行两个查询来执行计算:一个聚合您的顶级作者的#posts,另一个聚合计算所有作者的总帖子。另外,请注意,如果您需要做的只是对文档进行计数,则使用 count 函数是执行计算的一种非常有效的方法。 MongoDB 在 btree 索引中缓存计数,从而可以非常快速地对查询进行计数。

    如果这些聚合结果很慢,有几个策略。首先,请记住,如果适用,您希望使用 $match 开始查询以减少结果集。 $matches 也可以通过索引来加速。其次,您可以将这些计算作为预聚合执行。不必每次用户访问应用程序的某些部分时都运行这些聚合,而是让聚合在后台定期运行,并将聚合存储在包含预聚合值的集合中。这样,您的页面就可以简单地从该集合中查询预先计算的值。

    【讨论】:

    • 感谢您的回答。知道有用。在我的真实应用程序中寻求组合解决方案,例如在可能的情况下使用 $match,在可能的情况下进行预计算,以及在其他情况下不进行计数。上面的查询只是一个例子(因为我被要求提供代码)。
    • @Dylan 你知道这些改进是否已经完成了吗?
    【解决方案3】:

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

    这可以避免在多个请求中多次创建相同的阶段。

    【讨论】:

      【解决方案4】:

      如果您不想并行运行两个查询(一个用于聚合您的顶级作者的#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);
      });
      

      【讨论】:

        【解决方案5】:

        我遇到了同样的问题,用 $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/

        【讨论】:

          【解决方案6】:

          我得到了aggregate().toArray().length的总数

          【讨论】:

          • 这无法扩展。
          【解决方案7】:

          在我的例子中,我们使用 $out 阶段将结果集从 aggeration 转储到临时/缓存表中,然后对其进行计数。而且,由于我们需要对结果进行排序和分页,我们在临时表上添加索引并在会话中保存表名,在会话关闭/缓存超时时删除表。

          【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2015-06-09
          • 2012-09-24
          • 1970-01-01
          • 2021-06-15
          • 2013-05-29
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多