Beyond $match, $group, and $project, a handful of other stages cover most remaining real-world aggregation needs.
Work exactly like their find() equivalents, as pipeline stages — commonly placed after $group to order and cap a computed result:
db.orders.aggregate([
{ $group: { _id: "$customerId", total: { $sum: "$amount" } } },
{ $sort: { total: -1 } },
{ $limit: 5 }
])
// Top 5 customers by total spend$unwind takes a document with an array field and outputs one document per array element — necessary before you can $group by individual items inside that array:
// A document with tags: ["sale", "featured"]
// becomes two documents, one per tag, after $unwind
db.products.aggregate([
{ $unwind: "$tags" },
{ $group: { _id: "$tags", count: { $sum: 1 } } }
])$lookup is MongoDB's equivalent of a SQL JOIN — it pulls in matching documents from a different collection based on a shared field:
db.posts.aggregate([
{
$lookup: {
from: "authors",
localField: "authorId",
foreignField: "_id",
as: "authorInfo"
}
}
])The result adds an authorInfo array field to each post, containing the matching author document(s) — this is exactly the operation the referencing pattern from the schema-design lesson is built to support.
Put $match as early as the logic allows
Stage order matters for correctness, not just style — $match before $group filters the input; $match after $group filters the computed results. Putting filters as early as possible in the pipeline is also the standard performance habit.