Browse documentation
MongoDB TutorialVerified with mongodb 7.5.0

MongoDB Group By Examples with $group

Group MongoDB documents by fields, dates, and compound keys; calculate totals, averages, counts, and top values with correct aggregation pipeline ordering.

Understand what $group returns

MongoDB groups documents in an aggregation pipeline with the $group stage. Each input document contributes to a key stored in _id, and accumulator expressions calculate values for that group. The output contains one document per distinct key rather than the original documents.

Create a scoped order dataset:

const orders = db.collection("group_by_orders");

orders.deleteMany({ tutorial: "group-by" });
orders.insertMany([
  { customer: "Ada", region: "emea", status: "paid", total: 120, placedAt: ISODate("2026-09-01T10:00:00Z"), tutorial: "group-by" },
  { customer: "Ada", region: "emea", status: "paid", total: 80, placedAt: ISODate("2026-09-01T15:00:00Z"), tutorial: "group-by" },
  { customer: "Grace", region: "amer", status: "paid", total: 210, placedAt: ISODate("2026-09-02T09:30:00Z"), tutorial: "group-by" },
  { customer: "Grace", region: "amer", status: "cancelled", total: 45, placedAt: ISODate("2026-09-02T11:00:00Z"), tutorial: "group-by" },
  { customer: "Linus", region: "emea", status: "paid", total: 150, placedAt: ISODate("2026-09-03T12:00:00Z"), tutorial: "group-by" }
]);

The tutorial marker prevents the examples from grouping unrelated documents in the same collection.

Count documents by one field

Group paid orders by customer and count them:

orders.aggregate([
  { $match: { tutorial: "group-by", status: "paid" } },
  { $group: { _id: "$customer", orderCount: { $sum: 1 } } },
  { $sort: { orderCount: -1, _id: 1 } }
]);

Ada has two paid orders; Grace and Linus each have one. $sum: 1 adds one for every input document in the group. The final sort makes ties deterministic by customer name.

Place selective $match stages before $group when possible. This reduces the documents the grouping stage must process and can allow an appropriate index to support the initial filter.

Calculate totals and averages

Several accumulators can be computed in the same group:

orders.aggregate([
  { $match: { tutorial: "group-by", status: "paid" } },
  {
    $group: {
      _id: "$customer",
      orderCount: { $sum: 1 },
      revenue: { $sum: "$total" },
      averageOrder: { $avg: "$total" },
      smallestOrder: { $min: "$total" },
      largestOrder: { $max: "$total" }
    }
  },
  { $sort: { revenue: -1 } }
]);

Accumulator behavior depends on input types. A string value in total is not a numeric amount just because it looks like one. Validate important field types at write time and investigate legacy inconsistencies explicitly rather than hiding them with broad conversion.

For money, define the numeric representation used by the application. Binary floating-point, integers in minor units, and Decimal128 have different tradeoffs.

Group by more than one field

Use an embedded document as the group key:

orders.aggregate([
  { $match: { tutorial: "group-by" } },
  {
    $group: {
      _id: { region: "$region", status: "$status" },
      orderCount: { $sum: 1 },
      total: { $sum: "$total" }
    }
  },
  { $sort: { "_id.region": 1, "_id.status": 1 } }
]);

The result contains one group for each observed region/status combination. If the client needs flatter field names, reshape the output with $project:

{
  $project: {
    _id: 0,
    region: "$_id.region",
    status: "$_id.status",
    orderCount: 1,
    total: 1
  }
}

Add that stage after $group, before the final sort if the sort should use the projected names.

Group timestamps into calendar periods

Use $dateTrunc to derive an explicit calendar bucket:

orders.aggregate([
  { $match: { tutorial: "group-by", status: "paid" } },
  {
    $group: {
      _id: {
        $dateTrunc: {
          date: "$placedAt",
          unit: "day",
          timezone: "UTC"
        }
      },
      revenue: { $sum: "$total" },
      orders: { $sum: 1 }
    }
  },
  { $sort: { _id: 1 } }
]);

Calendar buckets depend on a timezone. Specify the business timezone rather than relying on an implicit assumption. Filter to the required date range before grouping so the server does not process unlimited history.

Keep the first or last document per group

$first and $last depend on input order. Sort before grouping when the requirement is chronological:

orders.aggregate([
  { $match: { tutorial: "group-by", status: "paid" } },
  { $sort: { customer: 1, placedAt: 1 } },
  {
    $group: {
      _id: "$customer",
      firstOrder: { $first: "$placedAt" },
      lastOrder: { $last: "$placedAt" },
      latestTotal: { $last: "$total" }
    }
  }
]);

Without the sort, “first” means first in the pipeline's current order, not earliest in business time. An index aligned with the match and sort may reduce work, but verify the actual plan.

Find top groups without losing ties accidentally

Sort grouped output and apply a limit:

orders.aggregate([
  { $match: { tutorial: "group-by", status: "paid" } },
  { $group: { _id: "$customer", revenue: { $sum: "$total" } } },
  { $sort: { revenue: -1, _id: 1 } },
  { $limit: 2 }
]);

This returns exactly two groups. If the business rule must include every customer tied at the boundary, a simple limit is not enough; define the tie behavior and use an additional pipeline strategy appropriate to the MongoDB server version.

Inspect execution and memory expectations

An index can help the early $match and sometimes the input sort. The grouping stage itself must maintain group state and may require substantial memory for high-cardinality keys. Use bounded time ranges, selective predicates, and representative data. Run explain("executionStats") to inspect the pipeline access path rather than timing one warm execution in a desktop client.

In MongoG, each aggregation returns a real cursor that can be inspected in pages. Avoid converting an unbounded production result to an array merely for convenience.

Practice and cleanup

Add two orders in another region, then group by region and day. Calculate paid revenue, cancelled value, and distinct customers. Explain whether a compound index on { tutorial: 1, status: 1, placedAt: 1 } supports the selective part of the pipeline and which work still happens in $group.

Remove the tutorial data when finished:

orders.deleteMany({ tutorial: "group-by" });

Continue with the full aggregation pipeline guide, date-range queries, and indexes with explain plans.