171

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

3
  • What would the results look like if there were only 2? Commented Dec 3, 2013 at 11:45
  • Take a look at $facet This may help stackoverflow.com/questions/61812361/… Commented May 15, 2020 at 6:38
  • Watch for those users of '$geoNear', because '$facet' is not compatible with it. Commented Sep 28, 2021 at 13:34

17 Answers 17

193

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}]
  }
]
Sign up to request clarification or add additional context in comments.

6 Comments

This works great, as of 3.4 this should be the accepted answer
To convert so arrayful result into simple two field object I need another $project?
This should be the accepted answer today. However, I found performance issues when using paging with $facet. The other up voted answer also has performance issues with $slice. I found it better to $skip and $limit in the pipeline and make a separate call for count. I tested this against fairly large data sets.
@SerG you can $unwind the totalCount
Regarding performance, the $facet stage, and its sub-pipelines, cannot make use of indexes, even if its sub-pipelines use $match or if $facet is the first stage in the pipeline. The $facet stage will always perform a COLLSCAN during execution.
|
117

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": [
      {...},
      {...},
      {...},
    ]
  }
]

5 Comments

Documentation on this: docs.mongodb.com/v3.2/reference/operator/aggregation/group/… ... note that with this approach, the entire non-paginated result set must fit in 16MB.
This is pure gold! I was going thru hell trying to make this work.
Thanks guy ! I juste need { $group: { _id: null, count: { $sum:1 }, result: { $push: '$$ROOT' }}} (insert after {$group:{}} for count total find.
How do you apply limit to the results set? Results is now a nested array
@valen You can see last line of code " 'results' => array( '$slice' => array( '$results', $skip, $length ) )" Here you can apply limit and skip params
78

Use this to find total count in resulting collection.

db.collection.aggregate( [
{ $match : { score : { $gt : 70, $lte : 90 } } },
{ $group: { _id: null, count: { $sum: 1 } } }
] );

2 Comments

Thanks. But, I have used "views" in my coding to get the count of the corresponding group count(i.e, group 1 => 2 records, group 3 => 5 records & so on). I want to get the records count(i.e, total: 120 records). Hope you understood..
Tricki one: { $sum: 1 }
51

You can use toArray function and then get its length for total records count.

db.CollectionName.aggregate([....]).toArray().length

6 Comments

While this might not work as a "proper" solution, it helped me debug something - it does work, even if it's not 100% a solution.
This isn't real solution.
TypeError: Parent.aggregate(...).toArray is not a function this is the error I gave with this solution.
This will fetch all the aggregated data then return the length of that array. not a good practice . instead u can add {$count: 'count'} in aggregation pipeline
Not sure that loading all the results is a smart idea to retrieve the totalCount.
|
44

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.

Comments

36

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)
}

2 Comments

This works just like a charm, but performance-wise is it good?
Will return empty array on no matches.
13

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);

1 Comment

fwiw you don't need the var declaration nor the map call. The first 3 lines of your first example is sufficient.
10
//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});

1 Comment

It is usually good practice to include explanatory text along with a code answer.
7

Solution provided by @Divergent does work, but in my experience it is better to have 2 queries:

  1. First for filtering and then grouping by ID to get number of filtered elements. Do not filter here, it is unnecessary.
  2. 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
            });
          }
      });
    }
  });
});

Comments

4

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
}

Comments

3

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) => {})

Comments

2

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
        }
    ]
}

1 Comment

I think it's better to use $ceil instead of $floor
2

It will first match and then give the matching condition total document count even your document count greater than 100k.

[
  {
    $match: {
      "Document.CompanyId": 12345
    }
  },
  {
    $count: 'Total_Count'
  }
]

Comments

1

If you don't want to group, then use the following method:

db.collection.aggregate( [ { $match : { score : { $gt : 70, $lte : 90 } } }, { $count: 'count' } ] );

1 Comment

I think the person asking the question does want to group, based on the subject.
0

Sorry, but I think you need two queries. One for total views and another one for grouped records.

You can find useful this answer

2 Comments

Thanks..I think so..But, there is no option with aggregation.. :(
i ran in to a similar situation. There was no answer but to do 2 querys. :( stackoverflow.com/questions/20113731/…
0

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
      }
    }
  }
])

Comments

0

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'
  }
]);

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.