Skip to content
ExponaBLOG
MONGODB, NOSQL & DATA MODELING · DAY 018 / 100

MongoDB aggregation examples: what developers should know

Pipelines you will reuse.

Expona Admin6 min read

MongoDB aggregation examples: what developers should know

Aggregation pipelines are one of the most powerful tools in MongoDB, yet many teams use them reactively only when a simple query won't cut it. We've found that learning a handful of reusable patterns early saves countless hours debugging slow queries or rebuilding logic in application code. This guide walks through the pipelines you will actually reuse in production.

Why aggregation matters for your team

When you move beyond single-document queries, you're often pushing work that belongs in the database into your application layer. Aggregation pipelines let you filter, transform, group, and reshape data at the source, reducing network traffic and keeping your business logic close to the data. We've seen teams cut response times by 60–80% by moving aggregations from Node.js or .NET into MongoDB.

The key is recognizing patterns early. Most real-world use cases boil down to a small set of pipeline shapes: filtering, grouping, unwinding, and faceting. Learn those shapes, and you'll recognize the solution the moment a requirement walks in.

Revenue by month — Group by date parts

One of the most common aggregations in e-commerce is rolling up revenue by time period. You need to know how much money came in each month, quarter, or week. The naive approach—fetch all orders and loop through them in your application—scales badly. A pipeline does it cleanly.

javascript
db.orders.aggregate([
  { $match: { status: "paid" } },
  { $group: { 
      _id: { $dateToString: { format: "%Y-%m", date: "$createdAt" } }, 
      total: { $sum: "$total" },
      count: { $sum: 1 }
    } 
  },
  { $sort: { _id: 1 } }
])

This pipeline does three things. First, $match filters to only paid orders—no point summing refunded or pending transactions. Second, $group buckets orders by year-month using $dateToString to extract just the date parts you care about, then sums the total field and counts documents. Finally, $sort arranges results chronologically.

The _id field in $group is your grouping key. When you set it to a string like "2024-01"code>, MongoDB groups all orders from January 2024 together. You can also group by multiple fields:

javascript
{ $group: { 
    _id: { 
      month: { $dateToString: { format: "%Y-%m", date: "$createdAt" } },
      currency: "$currency"
    }, 
    total: { $sum: "$total" }
  } 
}

Now you get revenue broken down by both month and currency, which is essential if your platform spans regions. In production, we often add a $project stage afterward to reshape the output for your API response.

Top products — $unwind items then $group by product

Orders typically contain arrays of items. If your orders collection looks like this:

javascript
{
  _id: ObjectId(...),
  createdAt: ISODate("2024-01-15"),
  items: [
    { productId: "SKU123", quantity: 2, price: 29.99 },
    { productId: "SKU456", quantity: 1, price: 49.99 }
  ],
  total: 109.97
}

Then finding your top 10 selling products requires $unwind. This operator flattens the array, creating one document per item:

javascript
db.orders.aggregate([
  { $match: { status: "paid" } },
  { $unwind: "$items" },
  { $group: { 
      _id: "$items.productId", 
      quantity: { $sum: "$items.quantity" },
      revenue: { $sum: { $multiply: ["$items.quantity", "$items.price"] } }
    } 
  },
  { $sort: { revenue: -1 } },
  { $limit: 10 }
])

After unwinding, each document now represents a single item. You group by productId, sum the quantities sold, and calculate revenue. The $multiply operator computes revenue per product on the fly. Finally, sort descending by revenue and take the top 10.

A common mistake here is forgetting that $unwind discards documents with empty or missing arrays. If you have orders without items (edge case, but real), add { preserveNullAndEmptyArrays: true } to $unwind if you need them.

Count by status — $group with $sum: 1

Sometimes you just need tallies. How many orders are in each status? A simple aggregation:

javascript
db.orders.aggregate([
  { $group: { 
      _id: "$status", 
      count: { $sum: 1 }
    } 
  },
  { $sort: { count: -1 } }
])

The $sum: 1 idiom increments a counter for each document. Without a $match stage, you're counting everything. Add one if you want to filter—for example, orders from the last 30 days.

You can also break down counts by multiple fields:

javascript
db.orders.aggregate([
  { $group: { 
      _id: { status: "$status", region: "$region" }, 
      count: { $sum: 1 }
    } 
  }
])

Now you see how many orders are pending, shipped, or delivered in each region. This pattern scales to your CRM too: count leads by source and stage, count support tickets by priority and assignee, and so on.

Facets — $facet runs several pipelines on the same input

Real dashboards rarely show just one metric. You want total orders, revenue by month, top products, and order status all at once. Rather than running four separate queries, use $facet:

javascript
db.orders.aggregate([
  { $match: { status: "paid" } },
  { $facet: {
      "revenueByMonth": [
        { $group: { 
            _id: { $dateToString: { format: "%Y-%m", date: "$createdAt" } }, 
            total: { $sum: "$total" }
          } 
        },
        { $sort: { _id: 1 } }
      ],
      "topProducts": [
        { $unwind: "$items" },
        { $group: { 
            _id: "$items.productId", 
            quantity: { $sum: "$items.quantity" }
          } 
        },
        { $sort: { quantity: -1 } },
        { $limit: 5 }
      ],
      "totalRevenue": [
        { $group: { 
            _id: null, 
            total: { $sum: "$total" }
          } 
        }
      ],
      "orderCount": [
        { $count: "count" }
      ]
    } 
  }
])

This single pipeline returns an object with four keys, each containing the result of a separate aggregation. Facets all run on the same filtered dataset (paid orders), so the initial $match applies to every sub-pipeline. The result is a single network round-trip to the database and a cohesive response for your dashboard.

Facets are especially powerful in e-commerce and SaaS dashboards. You can add filters at the top and get all your KPIs updated together. Teams often pair facets with caching—compute once every hour and serve from Redis—since the dataset doesn't change minute-to-minute.

Common mistakes

  • Grouping before matching. Filter early with $match. Putting it after $group is inefficient and confusing.
  • Forgetting indexes. Aggregation pipelines use indexes on fields in $match stages. Without them, even "simple" groupings scan the entire collection.
  • Unwinding large arrays carelessly. If your items array has thousands of elements, $unwind balloons your document count. Add a $match before unwinding when possible.
  • Over-complicating $group. Stick to summing, counting, averaging, and pushing values into arrays. Save complex logic for $project afterward.
  • Ignoring allowDiskUse. For large aggregations, add { allowDiskUse: true } to prevent out-of-memory errors. MongoDB spills to disk gracefully.

Key takeaways

  • Start with $match to filter early; it's the cheapest stage in the pipeline.
  • Use $dateToString to group orders and events by time period without complex date math.
  • Master $unwind for flattening arrays before grouping on nested fields.
  • $sum: 1 is your friend for counting documents and breaking down by status or category.
  • $facet lets you compute multiple aggregations in one query, cutting latency and simplifying your application logic.
  • Always check that indexes exist on fields you're matching and grouping on.
  • Test aggregations with allowDiskUse: true in staging if your dataset is large or your pipeline is complex.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles