当在MongoDB上运行普通的"find"查询时,我可以通过在返回的游标上运行"count"来获得总结果计数(无论限制如何)。所以,即使我将结果集限制为10(例如),我仍然可以知道结果的总数是53(例如)。
但是,如果我理解正确的话,聚合框架不会返回游标,而只是返回结果。因此,如果我使用$limit
管道运算符,我如何知道结果的总数,而不考虑所述限制?
我想我可以运行两次聚合(一次通过$group
计算结果,一次使用$limit
计算实际有限的结果),但这似乎效率低下。
另一种方法可以是在$limit
操作之前将结果总数附加到文档(通过$group
),但这似乎也没有效率,因为这个数字将附加到每个文档(而不是只为集合返回一次)。
我是不是遗漏了什么?有什么想法吗?谢谢
例如,如果这是查询:
db.article.aggregate(
{ $group : {
_id : "$author",
posts : { $sum : 1 }
}},
{ $sort : { posts: -1 } },
{ $limit : 5 }
);
我如何知道有多少结果可用(在$limit
之前)?结果不是一个游标,所以我不能只对它进行计数。
有一个使用推送和切片的解决方案:https://stackoverflow.com/a/39784851/4752635(@emaniacs在这里也提到了这一点)。
但我更喜欢使用2个查询。对于大型集合,推送$$ROOT并使用$slice的解决方案会遇到16MB的文档内存限制。此外,对于大型集合,两个查询加在一起似乎比使用$$ROOT推送的查询运行得更快。您也可以并行运行它们,因此您只受两个查询中较慢的查询(可能是排序的查询)的限制。
- 首先进行过滤,然后按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
});
}
});
}
});
});
Assaf,在不久的将来会对聚合框架进行一些增强,这可能会让您轻松地一次性完成计算,但现在,最好通过并行运行两个查询来执行计算:一个是聚合顶级作者的#posts,另一个是计算所有作者的总posts。此外,请注意,如果您只需要对文档进行计数,那么使用计数函数是执行计算的一种非常有效的方式。MongoDB在btree索引中缓存计数,从而可以非常快速地对查询进行计数。
如果这些聚合结果是缓慢的,那么有几个策略。首先,请记住,如果适用,您希望使用$match来启动查询,以减少结果集$匹配也可以通过索引来加速。其次,您可以将这些计算作为预聚合来执行。不要每次用户访问应用程序的某个部分时都运行这些聚合,而是让聚合在后台定期运行,并将聚合存储在包含预聚合值的集合中。这样,您的页面就可以简单地查询该集合中预先计算的值。
如果您不想并行运行两个查询(一个用于聚合顶级作者的#posts,另一个用于计算所有作者的总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);
});
$facets聚合操作可用于Mongo>=3.4版本。这允许在多个子管道中的管道的特定阶段分叉,在这种情况下,允许构建一个子管道来计算文档数量,另一个子管道用于排序、跳过和限制。
这样可以避免在多个请求中多次进行相同的阶段。
我也遇到了同样的问题,并用$project、$sice和$$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;
});
您需要声明from
和to
变量。
https://docs.mongodb.com/manual/reference/operator/aggregation/slice/
在我的例子中,我们使用$out stage将aggoration的结果集转储到临时/缓存表中,然后对其进行计数。由于我们需要对结果进行排序和分页,我们在临时表上添加索引,并在会话中保存表名,在会话关闭/缓存超时时删除该表。
我用aggregate().toArray().length
获得总数