MongoDB Aggregation: How to get total records count? - mongodb

I have used aggregation for fetching records from mongodb.
$result = $collection->aggregate(array(
array('$match' => $document),
array('$group' => array('_id' => '$book_id', 'date' => array('$max' => '$book_viewed'), 'views' => array('$sum' => 1))),
array('$sort' => $sort),
array('$skip' => $skip),
array('$limit' => $limit),
));
If I execute this query without limit then 10 records will be fetched. But I want to keep limit as 2. So I would like to get the total records count. How can I do with aggregation? Please advice me. Thanks

Since v.3.4 (i think) MongoDB has now a new aggregation pipeline operator named 'facet' which in their own words:
Processes multiple aggregation pipelines within a single stage on the same set of input documents. Each sub-pipeline has its own field in the output document where its results are stored as an array of documents.
In this particular case, this means that one can do something like this:
$result = $collection->aggregate([
{ ...execute queries, group, sort... },
{ ...execute queries, group, sort... },
{ ...execute queries, group, sort... },
{
$facet: {
paginatedResults: [{ $skip: skipPage }, { $limit: perPage }],
totalCount: [
{
$count: 'count'
}
]
}
}
]);
The result will be (with for ex 100 total results):
[
{
"paginatedResults":[{...},{...},{...}, ...],
"totalCount":[{"count":100}]
}
]

This is one of the most commonly asked question to obtain the paginated result and the total number of results simultaneously in single query. I can't explain how I felt when I finally achieved it LOL.
$result = $collection->aggregate(array(
array('$match' => $document),
array('$group' => array('_id' => '$book_id', 'date' => array('$max' => '$book_viewed'), 'views' => array('$sum' => 1))),
array('$sort' => $sort),
// get total, AND preserve the results
array('$group' => array('_id' => null, 'total' => array( '$sum' => 1 ), 'results' => array( '$push' => '$$ROOT' ) ),
// apply limit and offset
array('$project' => array( 'total' => 1, 'results' => array( '$slice' => array( '$results', $skip, $length ) ) ) )
))
Result will look something like this:
[
{
"_id": null,
"total": ...,
"results": [
{...},
{...},
{...},
]
}
]

Use this to find total count in resulting collection.
db.collection.aggregate( [
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] );

You can use toArray function and then get its length for total records count.
db.CollectionName.aggregate([....]).toArray().length

Here are some ways to get total records count while doing MongoDB Aggregation:
Using $count:
db.collection.aggregate([
// Other stages here
{ $count: "Total" }
])
For getting 1000 records this takes on average 2 ms and is the fastest way.
Using .toArray():
db.collection.aggregate([...]).toArray().length
For getting 1000 records this takes on average 18 ms.
Using .itcount():
db.collection.aggregate([...]).itcount()
For getting 1000 records this takes on average 14 ms.

Use the $count aggregation pipeline stage to get the total document count:
Query :
db.collection.aggregate(
[
{
$match: {
...
}
},
{
$group: {
...
}
},
{
$count: "totalCount"
}
]
)
Result:
{
"totalCount" : Number of records (some integer value)
}

I did it this way:
db.collection.aggregate([
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] ).map(function(record, index){
print(index);
});
The aggregate will return the array so just loop it and get the final index .
And other way of doing it is:
var count = 0 ;
db.collection.aggregate([
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] ).map(function(record, index){
count++
});
print(count);

//const total_count = await User.find(query).countDocuments();
//const users = await User.find(query).skip(+offset).limit(+limit).sort({[sort]: order}).select('-password');
const result = await User.aggregate([
{$match : query},
{$sort: {[sort]:order}},
{$project: {password: 0, avatarData: 0, tokens: 0}},
{$facet:{
users: [{ $skip: +offset }, { $limit: +limit}],
totalCount: [
{
$count: 'count'
}
]
}}
]);
console.log(JSON.stringify(result));
console.log(result[0]);
return res.status(200).json({users: result[0].users, total_count: result[0].totalCount[0].count});

Solution provided by #Divergent does work, but in my experience it is better to have 2 queries:
First for filtering and then grouping by ID to get number of filtered elements. Do not filter here, it is unnecessary.
Second query which filters, sorts and paginates.
Solution with pushing $$ROOT and using $slice runs into document memory limitation of 16MB for large collections. Also, for large collections two queries together seem to run faster than the one with $$ROOT pushing. You can run them in parallel as well, so you are limited only by the slower of the two queries (probably the one which sorts).
I have settled with this solution using 2 queries and aggregation framework (note - I use node.js in this example, but idea is the same):
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
});
}
});
}
});
});

This could be work for multiple match conditions
const query = [
{
$facet: {
cancelled: [
{ $match: { orderStatus: 'Cancelled' } },
{ $count: 'cancelled' }
],
pending: [
{ $match: { orderStatus: 'Pending' } },
{ $count: 'pending' }
],
total: [
{ $match: { isActive: true } },
{ $count: 'total' }
]
}
},
{
$project: {
cancelled: { $arrayElemAt: ['$cancelled.cancelled', 0] },
pending: { $arrayElemAt: ['$pending.pending', 0] },
total: { $arrayElemAt: ['$total.total', 0] }
}
}
]
Order.aggregate(query, (error, findRes) => {})

I needed the absolute total count after applying the aggregation. This worked for me:
db.mycollection.aggregate([
{
$group: {
_id: { field1: "$field1", field2: "$field2" },
}
},
{
$group: {
_id: null, count: { $sum: 1 }
}
}
])
Result:
{
"_id" : null,
"count" : 57.0
}

If you don't want to group, then use the following method:
db.collection.aggregate( [
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $count: 'count' }
] );

Here is an example with Pagination, match and sort in mongoose aggregate
const [response] = await Prescribers.aggregate([
{ $match: searchObj },
{ $sort: sortObj },
{
$facet: {
response: [{ $skip: count * page }, { $limit: count }],
pagination: [
{
$count: 'totalDocs',
},
{
$addFields: {
page: page + 1,
totalPages: {
$floor: {
$divide: ['$totalDocs', count],
},
},
},
},
],
},
},
]);
Here count is the limit of each page and page is the the page number. Prescribers is the model
This would return the records similar to this
"data": {
"response": [
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
}
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
},
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
}
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
},
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
},
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
}
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
},
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
}
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
},
{
"_id": "6349308c90e58c6820bbc682",
"foo": "bar"
},
],
"pagination": [
{
"totalDocs": 592438,
"page": 1,
"totalPages": 59243
}
]
}

Sorry, but I think you need two queries. One for total views and another one for grouped records.
You can find useful this answer

if you need to $match with nested documents then
https://mongoplayground.net/p/DpX6cFhR_mm
db.collection.aggregate([
{
"$unwind": "$tags"
},
{
"$match": {
"$or": [
{
"tags.name": "Canada"
},
{
"tags.name": "ABC"
}
]
}
},
{
"$group": {
"_id": null,
"count": {
"$sum": 1
}
}
}
])

I had to perform a lookup, match and then count the documents recieved. Here is how I achieved it using mongoose:
ModelName.aggregate([
{
'$lookup': {
'from': 'categories',
'localField': 'category',
'foreignField': '_id',
'as': 'category'
}
}, {
'$unwind': {
'path': '$category'
}
}, {
'$match': {
'category.price': {
'$lte': 3,
'$gte': 0
}
}
}, {
'$count': 'count'
}
]);

Related

What is the equivalent of findOne using .aggregate in Mongodb?

Basically with this I manage to return all my objects from a collection. How can I return a single element, for example
   in the style of findOne({_ id:" 5e82d378527bb420a4001aaf ")?
I know how to use $match, but this returns various results.
let _id="5e82d378527bb420a4001aaf"
Noticia.aggregate([
{
$addFields: {
like: {
$cond: [{ $in: [_id, "$likes"] }, true, false]
},
dislike: {
$cond: [{ $in: [_id, "$dislikes"] }, true, false]
}
}
}
], (err, noticia) => {
// console.log(trans);
if (err) {
return res.status(400).json({
ok: false,
err
});
}
return res.status(200).json({
ok: true,
data: noticia
});
})
Consider a sample names collection:
{ _id: 1, name: "Jack", favoriteColor: "blue" },
{ _id: 2, name: "James", favoriteColor: "red" },
{ _id: 3, name: "John", favoriteColor: "blue" }
and run the following three queries using findOne:
db.names.findOne( { _id: 1 } )
db.names.findOne()
db.names.findOne( { favoriteColor : "blue" } )
the result is same for the three queries:
{ "_id" : 1, "name" : "Jack", "favoriteColor" : "blue" }
The equivalent queries respectively using aggregation are the following, with the same result:
db.names.aggregate( [
{ $match: { _id: 1 } },
] )
db.names.aggregate( [
{ $limit: 1 }
] )
db.names.aggregate( [
{ $match: { "favoriteColor" : "blue" } },
{ $limit: 1 }
] )
db.collection.findOne definition says -
Returns one document that satisfies the specified query criteria on
the collection or view. If multiple documents satisfy the query, this
method returns the first document according to the natural order which
reflects the order of documents on the disk.
With findOne if no document is found it returns a null. But an aggregation returns a cursor, and you can apply the cursor methods on the result.
Just use $limit to limit no.of docs to be returned from aggregation pipeline :
Noticia.aggregate([
{
$addFields: {
like: {
$cond: [{ $in: [_id, "$likes"] }, true, false]
},
dislike: {
$cond: [{ $in: [_id, "$dislikes"] }, true, false]
}
}
} , {$limit :1} // will return first doc from aggregation result of '$addFields' stage
], (err, noticia) => {
// console.log(trans);
if (err) {
return res.status(400).json({
ok: false,
err
});
}
return res.status(200).json({
ok: true,
data: noticia
});
})
Or if you wanted to return random single doc try $sample.
//Express and mongoose
exports.getData = (req, res) => {
Noticia.aggregate([
{
$addFields: {
like: {
$cond: [{ $in: [_id, "$likes"] }, true, false]
},
dislike: {
$cond: [{ $in: [_id, "$dislikes"] }, true, false]
}
}
}
]
).then( data=>{
res.send(data[0]); //return one document in format { ..... }
}).catch(err => {
res.status(500).send({
message: err.message
});
});
}

How to get grand total of columns in mongodb aggregate

I am using mongodb aggregate to show reports. my query is below :
db.campaigns_report.aggregate([
{ '$match' : { } },
{ '$group' : {
'_id': '$campaignId',
'campaignName' : { '$first': '$campaignName' },
'impressions' : { '$sum': '$impressions' }
}
},
{ '$project' : {'campaignName': '$campaignName', 'impressions': '$impressions'} },
{ '$facet' : {
'metadata': [ { '$count': "total"} ],
'data': [ { '$skip': 0 },{ '$limit': 10 } ]
}
}
]);
In the above query I want total sum of impressions in $facet along with total document count.
Thanks in advance.
To get total sum of impressions along with total document count you need to run $group specifying null as grouping _id and use $sum operator twice:
db.campaigns_report.aggregate([
{ '$match' : { } },
{ '$group' : {
'_id': '$campaignId',
'campaignName' : { '$first': '$campaignName' },
'impressions' : { '$sum': '$impressions' }
}
},
{ '$project' : {'campaignName': '$campaignName', 'impressions': '$impressions'} },
{ '$facet' : {
'metadata': [ { '$group': { _id: null, total: { $sum: 1 }, totalImpressions: { $sum: '$impressions' } } } ],
'data': [ { '$skip': 0 },{ '$limit': 10 } ]
}
}
])
Mongo Playground

Return 5 elements for for each type with aggregation

How do I create an aggregate operation that shows me 5 for each type?
For example, what I need is to show 5 of type= 1 , 5 of type=2 and 5 of type=3.
I have tried:
db.items.aggregate([
{$match : { "type" : { $gte:1,$lte:3 }}},
{$project: { "type": 1, "subtipo": 1, "dateupdate": 1, "latide": 1, "long": 1, "view": 1,month: { $month: "$dateupdate" } }},
{$sort:{view: -1, dateupdate: -1}},
{$limit:5}
]);
After the $match pipeline, you need to do an initial group which creates an array of the original documents. After that you can $slice the array with the documents to return the 5 elements.
The intuition can be followed in this example:
db.items.aggregate([
{ '$match' : { 'type': { '$gte': 1, '$lte': 3 } } },
{
'$group': {
'_id': '$type',
'docs': { '$push': '$$ROOT' },
}
},
{
'$project': {
'five_docs': {
'$slice': ['$docs', 5]
}
}
}
])
The above will return the 5 documents unsorted in an array. If you need to return the TOP 5 documents in sorted order then you can introduce a $sort pipeline before grouping the docs that re-orders the documents getting into the $group pipeline by the type and dateupdate fields:
db.items.aggregate([
{ '$match' : { 'type': { '$gte': 1, '$lte': 3 } } },
{ '$sort': { 'type': 1, 'dateupdate': -1 } }, // <-- re-order here
{
'$group': {
'_id': '$type',
'docs': { '$push': '$$ROOT' },
}
},
{
'$project': {
'top_five': {
'$slice': ['$docs', 5]
}
}
}
])

mongodb aggregation query for field value length's sum

Say, I have following documents:
{name: 'A', fav_fruits: ['apple', 'mango', 'orange'], 'type':'test'}
{name: 'B', fav_fruits: ['apple', 'orange'], 'type':'test'}
{name: 'C', fav_fruits: ['cherry'], 'type':'test'}
I am trying to query to find the total count of fav_fruits field on overall documents returned by :
cursor = db.collection.find({'type': 'test'})
I am expecting output like:
cursor.count() = 3 // Getting
Without much idea of aggregate, can mongodb aggregation framework help me achieve this in any way:
1. sum up the lengths of all 'fav_fruits' field: 6
and/or
2. unique 'fav_fruit' field values = ['apple', 'mango', 'orange', 'cherry']
You need to $project your document after the $match stage and use the $size operator which return the number of items in each array. Then in the $group stage you use the $sum accumulator operator to return the total count.
db.collection.aggregate([
{ "$match": { "type": "test" } },
{ "$project": { "count": { "$size": "$fav_fruits" } } },
{ "$group": { "_id": null, "total": { "$sum": "$count" } } }
])
Which returns:
{ "_id" : null, "total" : 6 }
To get unique fav_fruits simply use .distinct()
> db.collection.distinct("fav_fruits", { "type": "test" } )
[ "apple", "mango", "orange", "cherry" ]
Do this to get just the number of fruits in the fav_fruits array:
db.fruits.aggregate([
{ $match: { type: 'test' } },
{ $unwind: "$fav_fruits" },
{ $group: { _id: "$type", count: { $sum: 1 } } }
]);
This will return the total number of fruits.
But if you want to get the array of unique fav_fruits along with the total number of elements in the fav_fruits field of each document, do this:
db.fruits.aggregate([
{ $match: { type: 'test' } },
{ $unwind: "$fav_fruits" },
{ $group: { _id: "$type", count: { $sum: 1 }, fav_fruits: { $addToSet: "$fav_fruits" } } }
])
You can try this. It may helpful to you.
db.collection.aggregate([{ $match : { type: "test" } }, {$group : { _id : null, count:{$sum:1} } }])

Aggregation Framework, Sort by array size including empty

I am trying to get a list of publications sort by number of tags. I've get some stuff working but the $unwind operator make disappear the publications with zero tags. I've tried to add a place holder to bypass that without success:
Publication.collection.aggregate(
{ "$project" => { tags: { "$push" => "holder" } } },
{ "$unwind" => '$tags' },
{ "$group" => { _id: '$_id', count: { "$sum" => 1 } } },
{ "$sort" => { count: 1 } }
)
I got:
failed with error 15999: "exception: invalid operator '$push'"
Documents exemples:
{ _id: '1', tags: ['b','c'] }
{ _id: '2', tags: ['a'] }
{ _id: '3' }
Any ideas?
You can't use $push in a $project pipeline stage; it's only for the $group stage. Unfortunately you can't just add a constant to the end of all the tags arrays in your aggregation pipeline.
This is inelegant, but I'd add a placeholder to all tags arrays in your collection itself:
db.collection.update({}, {$addToSet: {tags: null}}, false, true)
Then subtract 1 from the counts at the end of your pipeline:
db.collection.aggregate(
{ '$unwind' : '$tags' },
{ '$group' : { _id: '$_id', count: { '$sum' : 1 } } },
{ $project: { _id: true, count: { '$subtract': [ '$count', 1 ] } } },
{ '$sort' : { count: 1 } }
)
Vote for https://jira.mongodb.org/browse/SERVER-9334 to get a better method in the future.