MongoDB $lookup, $facet and advanced stages
$lookup, $facet, $bucket, $replaceRoot, $graphLookup, top-N per group. Β· 11 practice questions Β· Cheat sheet β
$lookup is how you join two collections in MongoDB: it matches localField in the current documents to foreignField in another collection and attaches the matches as an array, always an array, even for a single match. $unwind or $arrayElemAt turns that into a plain embedded document. $facet runs several sub-pipelines over the same input in one pass and returns them as one document, ideal for "results plus counts". $bucket groups numbers into ranges, $replaceRoot promotes a subdocument to the top level, and $graphLookup follows references recursively, such as a manager chain.
These stages combine: $lookup then $unwind then $group is the join-and-aggregate pattern, and $sort, $group with $push and $slice give top-N per group. The questions here are the ones that come up in real reporting work.
MongoDB $lookup and $facet practice questions
- Join
orderstocustomers: attach the matching customer document as an array field calledcustomer. medium orders - Produce one row per order with
_id, the customer'snameascustomerName, andtotalβ sorted by_idascending. hard orders - In one pipeline produce a document with two keys:
byBorough(a$sortByCountonborough) andtopSeats(the 2 restaurants with the most seats, projected tonameandseatsonly, no_id). hard restaurants - Bucket movies by
imdb.ratinginto the ranges [0,6), [6,7), [7,8), [8,10) with acountin each and the titles pushed intotitles. Useotheras the default bucket. hard movies - Use a single stage to rank boroughs by restaurant count, highest first. medium restaurants
- For each
borough, list the top 2 restaurants byseats. Output_id(the borough) andtopβ an array of the whole documents, sliced to 2. Sort boroughs by name ascending. hard restaurants - Promote each Manhattan restaurant's
addresssubdocument to be the top-level document. Sort bybuildingascending. medium restaurants - For employee
E6, walk the full management chain upward into a field calledchain. hard employees - Which single restaurant has the best (lowest) average grade score? Output
nameandavgScore, one row. hard restaurants - Compute monthly revenue: group delivered orders by the
YYYY-MMstring of theirdateas_id, summingtotalintorevenue, sorted by month. hard orders - Join order line items to
productsand report revenue per product name: unwinditems, look up the product by sku, and sumqty * priceintorevenue. Output_idas the product name, sorted by revenue descending. hard orders
Each question is answered in a live mongo-shell editor on this page; the result is checked against a reference solution.
$lookup, $facet, $bucket and $graphLookup cheat sheet
$lookup β the left outer join
{ $lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer" // always an ARRAY, even for one match
} },
{ $unwind: "$customer" } // flatten it back to an object
$bucket β histogram
{ $bucket: {
groupBy: "$imdb.rating",
boundaries: [0, 6, 7, 8, 10],
default: "unrated",
output: { count: { $sum: 1 }, titles: { $push: "$title" } }
} }
$facet β several pipelines, one pass
{ $facet: {
byBorough: [ { $sortByCount: "$borough" } ],
priciest: [ { $sort: { price: -1 } }, { $limit: 3 } ]
} }
Returns one document with an array per key. Great for dashboards.
$replaceRoot β promote a subdocument
{ $replaceRoot: { newRoot: "$address" } }
Top N per group
The pattern worth memorising:
{ $sort: { borough: 1, seats: -1 } },
{ $group: { _id: "$borough", top: { $push: "$$ROOT" } } },
{ $project: { top: { $slice: ["$top", 3] } } }
Sort first, group into an array, then $slice. $$ROOT is the whole document.
$graphLookup β recursive
{ $graphLookup: {
from: "employees",
startWith: "$reportsTo",
connectFromField: "reportsTo",
connectToField: "_id",
as: "chain"
} }
Walks the chain until it runs out. Use for org charts, categories, threads.
Common questions
How do I join two collections in MongoDB?
$lookup: { $lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "customer" } }. The matches arrive as the customer array. Follow with { $unwind: "$customer" } when you expect exactly one match per order.Why is $lookup always an array?
$lookup returns every match in an array, even when there is one or none. Flatten it with $unwind, or pick the first with { $arrayElemAt: ["$customer", 0] } in a $project or $addFields.How do I get the top N per group?
$push, then slice: sort by the ranking field, $group by the key pushing documents into an array, and $project with { $slice: ["$items", 3] }. MongoDB 5.2 and later also offer $topN accumulators that do it in one stage.Continue:β $unwind & Array Pipelines
MongoDB topics:Finding & ProjectionComparison OperatorsLogical OperatorsElement & TypePattern MatchingSort, Limit & SkipArray QueriesEmbedded DocumentsAggregation BasicsGrouping & Accumulators$unwind & Array PipelinesFull cheat sheet