Course

The Power of Aggregates in Mongoose

What is an aggregate?

Simply put- an aggregate in Mongoose is just a query like any other- but with a twist.

It allows you:

  • Group models or results from multiple queries together
  • Group data in a single collection together
  • Perform operations on the grouped data
  • Return the results in a single response

This would normally be done in multiple queries, but with aggregates, you can do it all in one go.

When do you use aggregates?

Aggregates are useful when you need to perform operations on multiple documents or collections. They are especially useful when you need to group data together and perform operations on the grouped data- or when we need to perform many operations in a single query.

How do you use aggregates?

Aggregates are used in Mongoose by using the aggregate method on a model. This method takes an array of stages as an argument. Each stage is an object that defines an operation to be performed on the data.

You can think of them like steps in a pipeline. Each stage takes the output of the previous stage and performs an operation on it. The final output is the result of all the stages combined.

Here's an example of an aggregate with 3 stages:

Let's first imagine some sample data.

Maybe we have a list of orders with a user, the status of the order, and a price.

//
[
  { "user": "Alex", "status": "complete", "price": 10 },
  { "user": "Alex", "status": "complete", "price": 20 },
  { "user": "Bob", "status": "pending", "price": 30 },
  { "user": "Bob", "status": "complete", "price": 40 },
  { "user": "Alex", "status": "pending", "price": 50 }
]

Let's say we wanted to see which users have spent the most money on completed orders.

With aggregates, we can perform several operations at once. We can filter by just the complete orders, group them by user, sum the prices for each user, and then sort the results by the total price.

We might end up with something like this:

//
[
  { "user": "Bob", "total": 40 },
  { "user": "Alex", "total": 30 }
]

In order to do something like that- we need several stages in our aggregate.

  1. We need to filter by the status of the order.
  2. We need to group the orders by user, and sum the prices for each user.
  3. We need to sort the results by the total price.

Here's how we might do that in Mongoose:

//
const result = await Model.aggregate([
  {
    $match: { status: "complete" },
  },
  {
    $group: {
      _id: "$user",
      total: { $sum: "$price" },
    },
  },
  {
    $sort: { total: -1 },
  },
]);

The three stages in this aggregate are:

//
[
  { "user": "Alex", "status": "complete", "price": 10 },
  { "user": "Alex", "status": "complete", "price": 20 },
  { "user": "Bob", "status": "pending", "price": 30 },
  { "user": "Bob", "status": "complete", "price": 40 },
  { "user": "Alex", "status": "pending", "price": 50 }
]

The match stage filters the data by the status of the order.

//
{
  $match: {
    status: "complete";
  }
}
//
[
  { "user": "Alex", "status": "complete", "price": 10 },
  { "user": "Alex", "status": "complete", "price": 20 },
  { "user": "Bob", "status": "complete", "price": 40 }
]

The group stage groups the orders by user and sums the prices for each user.

//
{
  $group: {
    _id: "$user",
    total: { $sum: "$price" }
  }
}
//
[
  { "user": "Alex", "total": 30 },
  { "user": "Bob", "total": 40 }
]

The sort stage sorts the results by the total price.

//
{
  $sort: {
    total: -1;
  }
}
//
[
  { "user": "Bob", "total": 40 },
  { "user": "Alex", "total": 30 }
]

Common Aggregates

There are many stages you can use in an aggregate. Here are some of the most common ones:

$match

The $match stage filters the data by a specified condition.

//
{
  // only match documents where the status is complete
  $match: {
    status: "complete";
  }
}

It's similar to the find method in Mongoose- we can use the same query syntax.

//
{
  // only match documents where the price is greater than 20
  $match: {
    price: {
      $gt: 20;
    }
  }
}

$group

The $group stage groups the data by a specified field and performs an operation on the grouped data.

//
{
  // group the orders by user and sum the prices for each user
  $group: {
    _id: "$user",
    total: { $sum: "$price" }
  }
}

The _id field is the field to group by, not necessarily the _id field of the document.

In the example above, we create a new field called total that sums the price field for each user.

We can also group by multiple fields. Let's say we had this sample data:

Orders
[
    {
      "user": "Alex",
      "promo": false,
      "status": "complete",
      "price": 10
    },
    {
      "user": "Alex",
      "promo": false,
      "status": "complete",
      "price": 20
    },
    {
      "user": "Bob",
      "promo": false,
      "status": "pending",
      "price": 30
    },
    {
      "user": "Bob",
      "promo": true,
      "status": "complete",
      "price": 40
    },
    {
      "user": "Alex",
      "promo": true,
      "status": "pending",
      "price": 50
    }
  ]

We can group the orders by user and promo, and sum the prices for each user and promo.

This would tell us how much each user spent on promo and non-promo orders.

//
{
  // group the orders by user and status and sum the prices for each user and status
  $group: {
    _id: { user: "$user", promo: "$promo" },
    total: { $sum: "$price" }
  }
}

This would return something like this:

//
[
  {
    "_id": {
      "used_promotion": true,
      "user": "Alex"
    },
    "total": 50
  },
  {
    "_id": {
      "used_promotion": false,
      "user": "Bob"
    },
    "total": 30
  },
  {
    "_id": {
      "used_promotion": true,
      "user": "Bob"
    },
    "total": 40
  },
  {
    "_id": {
      "used_promotion": false,
      "user": "Alex"
    },
    "total": 30
  }
]

In the above examples we're adding a field which is the sum of the field for each group.

The aggregate stage can add new fields to our results and perform operations on the grouped data.

We can ese all kinds of operators and as many extra fields as we need in the stage:

//
{
  $group: {
    _id: { user: "$user", promo: "$promo" },
    totalPrice: { $sum: "$price" },
    avgPrice: { $avg: "$price" },
    maxPrice: { $max: "$price" },
    minPrice: { $min: "$price" }
  }
}

And each result would have all of these fields:

//
[
  {
    "_id": {
      "user": "Alex",
      "promo": false
    },
    "totalPrice": 30,
    "avgPrice": 15,
    "maxPrice": 20,
    "minPrice": 10
  }
  //...etc...
]

$sort

The $sort stage sorts the results by a specified field.

It allows you to change the order of the results to either ascending or descending.

//
{
  // sort the results by the total price in descending order
  $sort: {
    price: -1;
  }
}

// or

{
  // sort the results by the total price in ascending order
  $sort: {
    price: 1;
  }
}

We can even use the fields we created in the stage to sort the results if the stage comes after the stage.

//
[
  { "user": "Alex", "status": "complete", "price": 10 },
  { "user": "Alex", "status": "complete", "price": 20 },
  { "user": "Bob", "status": "pending", "price": 30 },
  { "user": "Bob", "status": "complete", "price": 40 },
  { "user": "Alex", "status": "pending", "price": 50 }
]

The group stage groups the orders by user and max price for each user.

The sort stage sorts the results by the max price.

//
{
  $group: {
    _id: "$user",
    biggestPurchase: { $max: "$price" }
  }
},
{
  $sort: { biggestPurchase: -1 }
}
//
[
  { "user": "Alex", "biggestPurchase": 50 },
  { "user": "Bob", "biggestPurchase": 40 }
]

$lookup

The $lookup stage performs a join with another collection.

This is useful when you need to combine data from multiple collections.

How does the $lookup stage work?

The $lookup stage takes an object with the following properties:

- The name of the collection to join with.

- The field in the current collection to join on.

- The field in the other collection to join on.

- The name of the field to add the joined data to.

Example

Suppose our orders collection has a userId field that references the _id field of the users collection.

We can use the $lookup stage to get the user information for each order.

//
"Order" :[
  { "userId": "123", "status": "complete", "price": 10 },
  { "userId": "456", "status": "complete", "price": 20 },
  { "userId": "789", "status": "pending", "price": 30 }
]

"User" :[
{ "_id": "123", "name": "Alex" },
{ "_id": "456", "name": "Bob" },
{ "_id": "789", "name": "Charlie" }
]
//
const result = await Order.aggregate([
  {
    $lookup: {
      from: "User",
      localField: "userId",
      foreignField: "_id",
      as: "user",
    },
  },
]);

//
[
  {
    "userId": "123",
    "status": "complete",
    "price": 10,
    "user": [
      { "_id": "123", "name": "Alex" }
    ]
  },
  {
    "userId": "456",
    "status": "complete",
    "price": 20,
    "user": [
      { "_id": "456", "name": "Bob" }
    ]
  },
  {
    "userId": "789",
    "status": "pending",
    "price": 30,
    "user": [
      { "_id": "789", "name": "Charlie" }
    ]
  }
]

Combining lookup with other stages

We can combine the $lookup stage with other stages to perform operations on the joined data.

For example, we can use the $group stage to group the orders by user and sum the prices for each user.

Insight

Remember, aggregates are like a pipeline- the output of one stage is the input of the next stage.

So if we want to group the orders by user after we've joined the user data, we need to put the $group stage after the $lookup stage.

//
const result = await Order.aggregate([
  {
    $lookup: {
      from: "User",
      localField: "userId",
      foreignField: "_id",
      as: "user",
    },
  },
  {
    $group: {
      _id: "$user.name",
      total: { $sum: "$price" },
    },
  },
]);

We would get results like this:

//
[
  {
    "_id": ["Alex"],
    "total": 10
  },
  {
    "_id": ["Bob"],
    "total": 20
  },
  {
    "_id": ["Charlie"],
    "total": 30
  }
]

What's going on here?

In the stage, we join the orders collection with the users collection on the userId field in the orders collection and the _id field in the users collection.

In the stage, we group the orders by user name and sum the prices for each user.

The stage uses the user.name field from the joined data to group the orders by user name. Meaning we key into the name field of the user array that was added by the stage.

Limit and Skip

The $limit and $skip stages allow you to limit the number of results returned and skip a specified number of results.

//
const result = await Order.aggregate([
  {
    $limit: 2,
  },
]);

This would return the first 2 results from the orders collection.

//
const result = await Order.aggregate([
  {
    $skip: 2,
  },
]);

This would skip the first 2 results and return the rest.

$project

The $project stage allows you to include or exclude fields from the results.

//
const result = await Order.aggregate([
  {
    $project: {
      user: 1,
      price: 1,
    },
  },
]);

This would only include the user and price fields in the results.

//
[
  { "user": "Alex", "price": 10 },
  { "user": "Alex", "price": 20 },
  { "user": "Bob", "price": 30 },
  { "user": "Bob", "price": 40 },
  { "user": "Alex", "price": 50 }
]

can also be used to run operations on fields and create new fields.

//
const result = await Order.aggregate([
  {
    $project: {
      user: 1,
      price: 1,
      priceWithFee: { $add: ["$price", 5] },
    },
  },
]);

Conclusion

Aggregates in Mongoose are a powerful tool for performing operations on multiple documents or collections. These are just some of the basic tools we have, but by using some of these tools as a reference, we can build really complex queries including our pagination section.

In the next lessons, we'll dive into developing pagination for our application!

0 Comments

"Please login to view comments"

glass-bbok

Join the Conversation!

Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.

Upgrade your account
tick-guideNext Lesson

Pagination and Filtering: Part One