MongoDB Aggregate Count: countDocuments(), $count and Group By Counts

Counting documents sounds like a one-liner until a report wants totals per state, per status and per date range at once. MongoDB gives you countDocuments(), estimatedDocumentCount(), the aggregate $count stage and $group, and each fits a different job.

In this tutorial, I’ll cover each MongoDB aggregate count pattern on one small orders collection, with real mongosh output from MongoDB 9.0.

Quick answer: for one filtered total, run db.orders.countDocuments({ status: "Shipped" }). Inside a pipeline, end with { $count: "shippedOrders" } after a $match; for a count per value, use $group with { $sum: 1 }.

Sample orders collection for the MongoDB count examples

All the queries below use this ten-document orders collection (database dfq_mongocount): US customers, states, 2026 dates, USD totals, a status and an items array.

db.orders.drop();
db.orders.insertMany([
  { _id: 1,  customer: "Megan Bowen",     state: "TX", orderDate: ISODate("2026-01-05"), total: 249.99,  status: "Shipped",  items: ["laptop stand", "usb-c hub"] },
  { _id: 2,  customer: "Alex Wilber",     state: "CA", orderDate: ISODate("2026-01-09"), total: 1180.00, status: "Shipped",  items: ["monitor", "keyboard", "mouse"] },
  { _id: 3,  customer: "Adele Vance",     state: "WA", orderDate: ISODate("2026-01-14"), total: 89.50,   status: "Canceled", items: ["webcam"] },
  { _id: 4,  customer: "Lee Gu",          state: "TX", orderDate: ISODate("2026-01-20"), total: 615.25,  status: "Shipped",  items: ["desk chair"] },
  { _id: 5,  customer: "Lidia Holloway",  state: "CA", orderDate: ISODate("2026-02-02"), total: 432.10,  status: "Pending",  items: ["headset", "dock"] },
  { _id: 6,  customer: "Diego Siciliani", state: "NY", orderDate: ISODate("2026-02-11"), total: 958.40,  status: "Shipped",  items: ["tablet", "case", "stylus"] },
  { _id: 7,  customer: "Isaiah Langer",   state: "TX", orderDate: ISODate("2026-02-18"), total: 77.80,   status: "Canceled", items: ["mouse pad"] },
  { _id: 8,  customer: "Johanna Lorenz",  state: "NY", orderDate: ISODate("2026-03-03"), total: 310.00,  status: "Pending",  items: ["printer"] },
  { _id: 9,  customer: "Nestor Wilke",    state: "FL", orderDate: ISODate("2026-03-09"), total: 529.00,  status: "Shipped",  items: ["router", "cables"] },
  { _id: 10, customer: "Patti Fernandez", state: "CA", orderDate: ISODate("2026-03-16"), total: 64.99,   status: "Shipped",  items: ["charger"] }
]);

Paste it into mongosh, or see how to import JSON into MongoDB from a file.

MongoDB countDocuments() with and without a filter

countDocuments(query, options) counts the documents that match a query. Pass {} to count the whole collection.

// Count every order, then only the matching ones
print("All orders:", db.orders.countDocuments({}));
print("Shipped:", db.orders.countDocuments({ status: "Shipped" }));
print("TX over $200:", db.orders.countDocuments({
  state: "TX", total: { $gt: 200 }
}));

// Fast total from collection metadata (no filter)
print("Estimated:", db.orders.estimatedDocumentCount());

Output:

All orders: 10
Shipped: 6
TX over $200: 2
Estimated: 10
MongoDB countDocuments with and without a filter and estimatedDocumentCount in mongosh
countDocuments() returns 10, 6 and 2; estimatedDocumentCount() returns 10 from metadata.

There are 10 orders, 6 Shipped, and 2 Texas orders over $200. Filters work like find(), so you can stack multiple conditions.

Under the hood, countDocuments() runs a $match plus a $group with $sum: 1. That’s why it’s always exact and works inside transactions.

estimatedDocumentCount() for a fast total

It takes no filter and reads the count from collection metadata, so it’s instant even on huge collections. Note: the docs warn it can be off after an unclean shutdown or with orphaned documents on a sharded cluster.

Is count() deprecated in MongoDB?

Partly. The MongoDB 9.0 manual says the drivers deprecate their count() APIs in favor of countDocuments() and estimatedDocumentCount(). The mongosh method still works, but it prints a warning:

// Legacy methods: still in mongosh, deprecated in the drivers
print("count():", db.orders.count());
print("count(filter):", db.orders.count({ status: "Shipped" }));
print("cursor + limit(2):",
  db.orders.find({ status: "Shipped" }).limit(2).count());

// The replacement takes limit as an option
print("countDocuments + limit:",
  db.orders.countDocuments({ status: "Shipped" }, { limit: 2 }));

Output:

DeprecationWarning: Collection.count() is deprecated. Use countDocuments or estimatedDocumentCount.
count(): 10
count(filter): 6
cursor + limit(2): 2
countDocuments + limit: 2
MongoDB count() deprecation warning and cursor.count() with limit in mongosh 2.14
mongosh warns that Collection.count() is deprecated; cursor.count() honors limit(2).

Note that find().limit(2).count() returns 2: mongosh ignores the old applySkipLimit flag and applies limit, unlike the legacy mongo shell. In new code, pass limit to countDocuments() instead.

Not sure which shell you have? Here’s how to check if MongoDB is installed.

MongoDB aggregate $count stage after $match

{ $count: "<field>" } outputs one document with the number of documents that reached it. A $match in front gives you a count with a condition.

// Shipped orders
printjson(db.orders.aggregate([
  { $match: { status: "Shipped" } },
  { $count: "shippedOrders" }
]).toArray());

// Orders over $500 placed on or after 02/01/2026
printjson(db.orders.aggregate([
  { $match: {
      total: { $gt: 500 },
      orderDate: { $gte: ISODate("2026-02-01") }
  } },
  { $count: "bigOrdersSinceFeb" }
]).toArray());

Output:

[
  {
    shippedOrders: 6
  }
]
[
  {
    bigOrdersSinceFeb: 2
  }
]
MongoDB aggregate $count stage after $match with greater than and date conditions
The first pipeline returns shippedOrders: 6; the second returns bigOrdersSinceFeb: 2.

Six orders are Shipped, and two are over $500 and dated 02/01/2026 or later. Any operator works in the $match, including the $in operator.

Count by field: $group with $sum: 1 vs the $count accumulator

To count per value, group on the field. Write the counter as { $sum: 1 } or, in MongoDB 5.0 and later, the { $count: {} } accumulator (no arguments, empty braces required).

db.orders.aggregate([
  { $group: {
      _id: "$state",
      orders: { $sum: 1 },        // classic way
      viaCount: { $count: {} }    // MongoDB 5.0+
  } },
  { $sort: { orders: -1, _id: 1 } }
]).forEach(d => print(d._id, d.orders, d.viaCount));

Output:

CA 3 3
TX 3 3
NY 2 2
FL 1 1
WA 1 1
MongoDB group by count with $sum: 1 and the $count accumulator side by side
$sum: 1 and $count: {} give the same per-state counts: CA 3, TX 3, NY 2, FL 1, WA 1.

California and Texas have 3 orders each, and both counters agree. More patterns are in the MongoDB group by count guide and in group by multiple fields.

How do you count distinct values in MongoDB?

Use distinct().length, the short form of a distinct query, or group on the field and count the groups.

// Small result sets: distinct() + length
print("All states:", db.orders.distinct("state").length);
print("Shipped to:", db.orders.distinct("state", { status: "Shipped" }).length);

// Any size: $group on the field, then $count the groups
printjson(db.orders.aggregate([
  { $group: { _id: "$state" } },
  { $count: "distinctStates" }
]).toArray());

Output:

All states: 5
Shipped to: 4
[
  {
    distinctStates: 5
  }
]

Orders come from 5 states; Shipped orders went to 4. Important: distinct() returns one document capped at 16 MB, so use the $group version when there are many unique values.

Count with conditions using $cond and $sum

Several conditional counts fit in one $group: each $cond adds 1 when its test is true, 0 otherwise.

printjson(db.orders.aggregate([
  { $group: {
      _id: null,
      shipped:  { $sum: { $cond: [{ $eq: ["$status", "Shipped"] }, 1, 0] } },
      canceled: { $sum: { $cond: [{ $eq: ["$status", "Canceled"] }, 1, 0] } },
      over500:  { $sum: { $cond: [{ $gt: ["$total", 500] }, 1, 0] } }
  } },
  { $project: { _id: 0 } }
]).toArray());

Output:

[
  {
    shipped: 6,
    canceled: 2,
    over500: 4
  }
]

One pass returns 6 Shipped, 2 Canceled and 4 orders over $500, like SQL’s SUM(CASE WHEN ...). The final $project hides _id, much like returning specific fields.

Multiple counts in one query with $facet

$facet runs several sub-pipelines on the same documents, so a dashboard gets a total and a breakdown in one round trip.

const [r] = db.orders.aggregate([
  { $match: { orderDate: { $gte: ISODate("2026-02-01") } } },
  { $facet: {
      total:    [{ $count: "n" }],
      byStatus: [{ $group: { _id: "$status", n: { $sum: 1 } } },
                 { $sort: { _id: 1 } }],
      over500:  [{ $match: { total: { $gt: 500 } } },
                 { $count: "n" }]
  } }
]).toArray();

print("Since 02/01/2026:", r.total[0].n);
r.byStatus.forEach(s => print(" ", s._id, s.n));
print("Over $500:", r.over500[0].n);

Output:

Since 02/01/2026: 6
  Canceled 1
  Pending 2
  Shipped 3
Over $500: 2
MongoDB $facet returning a total count, counts by status and a filtered count in one query
One $facet query returns 6 orders since 02/01/2026, the status breakdown and 2 orders over $500.

Six orders since 02/01/2026: 1 Canceled, 2 Pending, 3 Shipped, and 2 over $500. Put the shared $match before $facet; stages inside a facet can’t use indexes. Date filters are covered in order by date.

Count array elements with $size

$size returns an array’s length; wrap it in $sum to add lengths across documents.

// Items per order for California
db.orders.aggregate([
  { $match: { state: "CA" } },
  { $project: { _id: 0, customer: 1, itemCount: { $size: "$items" } } }
]).forEach(d => print(d.customer, d.itemCount));

// Items across all orders
printjson(db.orders.aggregate([
  { $group: { _id: null, totalItems: { $sum: { $size: "$items" } } } }
]).toArray());

Output:

Alex Wilber 3
Lidia Holloway 2
Patti Fernandez 1
[
  {
    _id: null,
    totalItems: 17
  }
]

The California orders hold 3, 2 and 1 items, and all 10 orders hold 17. Note: $size errors on a missing field, so use { $size: { $ifNull: ["$items", []] } } when needed.

To pull items out instead of counting them, see returning only matching array elements.

MongoDB count performance: indexes and COUNT_SCAN

A filtered count is only as fast as its query. With an index on the filtered field, MongoDB counts index keys without reading documents.

const plan = (filter) => {
  const ex = db.orders.explain("executionStats")
    .aggregate([{ $match: filter }, { $count: "n" }]);
  let s = ex.queryPlanner.winningPlan.queryPlan;
  while (s.inputStage) s = s.inputStage;
  print(s.stage, "| keys:", ex.executionStats.totalKeysExamined,
    "| docs:", ex.executionStats.totalDocsExamined);
};
plan({ status: "Shipped" });              // no index yet
db.orders.createIndex({ status: 1 });
plan({ status: "Shipped" });              // with the index

Output:

COLLSCAN | keys: 0 | docs: 10
COUNT_SCAN | keys: 6 | docs: 0
MongoDB count explain plan changing from COLLSCAN to COUNT_SCAN after creating an index
Before the index the count scans 10 documents; after it, COUNT_SCAN reads 6 keys and 0 documents.

Without an index it’s a COLLSCAN over 10 documents. After I create an index on status, it’s a COUNT_SCAN with 0 documents examined.

For an unfiltered total, estimatedDocumentCount() wins; countDocuments({}) scans the collection unless you add { hint: "_id_" }.

Common MongoDB count errors and how to fix them

MongoServerError: $where is not allowed in this context

db.orders.countDocuments({ $where: "this.total > 500" });

Error:

MongoServerError: $where is not allowed in this context

countDocuments() rejects $where, $near and $nearSphere. Use $expr, which also lets you compare two fields:

print(db.orders.countDocuments({ $expr: { $gt: ["$total", 500] } }));

Output:

4

MongoServerError: the count field cannot be a $-prefixed path

db.orders.aggregate([{ $count: "$total" }]);

Error:

MongoServerError: the count field cannot be a $-prefixed path

$count wants a plain name like "orderCount": not empty, no leading $, no dot.

Why does a MongoDB count return 0 or nothing?

// Typo in the collection name: no error, just 0
print("db.order:", db.order.countDocuments({}));

// $count with no matching documents returns no document at all
printjson(db.orders.aggregate([
  { $match: { state: "OR" } },
  { $count: "oregonOrders" }
]).toArray());

Output:

db.order: 0
[]

A misspelled collection returns 0 with no error, and $count returns an empty array when nothing matches. Check the name with show collections and treat an empty result as zero.

Which MongoDB count method should you use?

You need…Use
Fast total, no filterestimatedDocumentCount()
Exact count of matching documentscountDocuments(filter)
A count at the end of a pipeline$match + $count
Count per value (group by)$group + $sum: 1 or $count: {}
Distinct valuesdistinct().length or $group + $count
Several counts at once$sum + $cond, or $facet
Items inside an array$size

I default to countDocuments() for single totals and $group for anything per value. The usual mistakes are count() in driver code and expecting $count to return 0.

These guides pair well with your count queries:

Frequently Asked Questions

What is the difference between count() and countDocuments() in MongoDB?

count() without a filter reads collection metadata and can be inaccurate; countDocuments() runs an aggregation and is exact. Drivers deprecate count(), as the countDocuments() documentation notes.

Why does $count return nothing instead of 0?

If no documents reach the $count stage, it outputs no document, so you get an empty array.

How do I count documents in MongoDB Compass?

The Documents tab shows the count and updates when you apply a filter. For grouped counts, add $match and $count or $group stages in the Aggregations tab.

How do I count documents in MongoDB with Python?

In PyMongo, use collection.count_documents(filter) or collection.estimated_document_count(), and collection.aggregate() for group-by counts.

Is $count faster than countDocuments()?

No. countDocuments() is itself a $match plus $group pipeline. An index on the filtered field is what makes either one fast.