MongoDB Query to Find Top N Records Per Group
$group to group documents and $push with $slice to keep top N sorted items per group, like: db.collection.aggregate([{ $sort: { groupField: 1, sortField: -1 } }, { $group: { _id: "$groupField", topN: { $push: "$ROOT" } } }, { $project: { topN: { $slice: ["$topN", n] } } }]).Examples
How to Think About It
Algorithm
Code
db.collection.aggregate([
{ $sort: { group: 1, score: -1 } },
{ $group: {
_id: "$group",
topScores: { $push: "$$ROOT" }
}
},
{ $project: {
topScores: { $slice: ["$topScores", 2] }
}
}
])Dry Run
Let's trace the example with groups A and B and scores through the aggregation pipeline.
Sort documents
Sort by group ascending and score descending: [{group: 'A', score: 20}, {group: 'A', score: 10}, {group: 'B', score: 15}, {group: 'B', score: 5}]
Group by 'group' field
Group A: [{group: 'A', score: 20}, {group: 'A', score: 10}], Group B: [{group: 'B', score: 15}, {group: 'B', score: 5}]
Slice top 2 per group
Keep first 2 items per group as topScores array
| Group | Top Scores |
|---|---|
| A | [{group: 'A', score: 20}, {group: 'A', score: 10}] |
| B | [{group: 'B', score: 15}, {group: 'B', score: 5}] |
Why This Works
Step 1: Sorting
Sorting ensures that when we group, the documents are in the order we want to pick the top n from.
Step 2: Grouping
Grouping collects all documents of the same group into one place to process them together.
Step 3: Slicing
Slicing the array keeps only the top n documents per group, discarding the rest.
Alternative Approaches
db.collection.aggregate([
{ $setWindowFields: {
partitionBy: "$group",
sortBy: { score: -1 },
output: { rank: { $rank: {} } }
}
},
{ $match: { rank: { $lte: 2 } } }
])db.collection.aggregate([
{ $facet: {
groupA: [ { $match: { group: 'A' } }, { $sort: { score: -1 } }, { $limit: 2 } ],
groupB: [ { $match: { group: 'B' } }, { $sort: { score: -1 } }, { $limit: 2 } ]
}
}
])Complexity: O(m log m) time, O(m) space
Time Complexity
Sorting the entire collection by group and score dominates with O(m log m), where m is the number of documents.
Space Complexity
Grouping stores arrays of documents per group, which can use O(m) space in the worst case.
Which Approach is Fastest?
Using $setWindowFields can be faster for large datasets as it avoids grouping large arrays, but requires MongoDB 5.0+. The $group and $push method is more widely supported but can be slower with large groups.
| Approach | Time | Space | Best For |
|---|---|---|---|
| $group with $push and $slice | O(m log m) | O(m) | General use, compatible with older MongoDB versions |
| $setWindowFields with $rank | O(m log m) | O(m) | Efficient ranking, requires MongoDB 5.0+ |
| $facet with separate pipelines | O(k * n log n) | O(n) | Few known groups, manual approach |