Aggregation Sorting & Limiting
Master MongoDB's aggregation framework for sorting and limiting results. This lesson covers the $sort and $limit stages, practical examples, performance tips, and common pitfalls — ideal for developers advancing through the MongoDB learning path.
Focus: aggregation framework sorting limiting
Sorting and limiting documents with MongoDB's find() method works fine for quick queries, but once you need to sort by computed values, aggregate across multiple documents, or paginate through millions of records, the aggregation framework's $sort and $limit stages become your best friends. In this lesson, you'll move beyond basic sort() and limit() chains to harness the full power of the aggregation pipeline for sorting and limiting — the same techniques used in production systems for leaderboards, recent activity feeds, and paginated APIs. By the end, you'll not only understand why the aggregation framework shines for these tasks, but you'll be able to apply it confidently in your own projects.
The problem this lesson solves
You've probably written queries like db.products.find().sort({price: -1}).limit(10) — it's simple, and it works. But what happens when you need to sort by a field that doesn't exist yet, like a computed discount price, or when you need to find the top 10 customers by total order value across thousands of orders? The find() method can't do that in one query; you'd need to either store denormalized values or perform multiple queries and merge results in application code — slow, brittle, and hard to maintain.
The aggregation framework solves this by letting you transform, sort, and limit documents inside the database, delivering only the final result to your application. This means less data transferred over the wire, faster response times, and cleaner code. Without it, you'd be stuck writing inefficient application-side logic or juggling multiple queries — a pain point every MongoDB developer hits sooner or later.
Core concept / mental model
Think of the aggregation pipeline as an assembly line for your data. Each stage is a workstation that takes a stream of documents, does something to them, and passes them to the next workstation. The $sort stage is a workstation that reorders documents based on a key, and $limit is a gate that only lets the first N documents through.
Key terms:
- Pipeline: An array of stages processed in order —
[ { $match: ... }, { $sort: ... }, { $limit: ... } ]. - Stage: A single operation such as
$match,$sort,$limit,$group,$project. $sortstage: Sorts documents by one or more fields. Use1for ascending,-1for descending.$limitstage: Caps the number of documents passed to the next stage.
A mental picture: imagine you're selecting the top 5 highest-paid employees from a spreadsheet. First, you filter out inactive employees ($match), then you sort by salary descending ($sort), then you take only the first 5 rows ($limit). Each step is independent, but together they produce a precise result.
Pro tip: Aggregation stages are lazy — MongoDB optimizes the pipeline behind the scenes. For instance, if you put
$limitbefore an expensive$unwind, MongoDB may reorder stages to reduce work, but always write stages in a logical order for readability.
How it works step by step
Let's walk through how $sort and $limit work together, step by step, with a concrete scenario.
Scenario: You have a products collection with fields name, price, stock, and category. You want the 3 cheapest items that are in stock.
- Start with a collection: All documents initially stream into the pipeline.
- Match stage: Filter documents to only those with
stock > 0— this reduces the dataset early. - Sort stage: Sort the remaining documents by
priceascending. MongoDB uses an in-memory sort if the data fits within the 100 MB limit, otherwise it spills to disk (you'll see how to handle that later). - Limit stage: Take only the first 3 documents from the sorted stream.
- Output: The aggregation returns an array of up to 3 documents.
Each stage's output is the next stage's input. If you misorder stages, you get different results — for example, limiting before sorting would give you any 3 documents (the first ones encountered) and then sort only those 3, which is rarely what you want.
Pagination pattern: To implement "load more" or paged results, you combine $sort and $limit with a $skip stage. The order matters — always sort first, then skip, then limit. Skipping before sorting yields inconsistent ordering across pages.
Performance note: Sorting on an indexed field lets MongoDB avoid an in-memory sort and makes the pipeline faster, especially with large datasets. If you sort by a non-indexed field, MongoDB performs a blocking sort (waits for all documents) and may use disk — we'll cover that in troubleshooting.
Hands-on walkthrough
Let's get our hands dirty. We'll use a collection of orders to find the top 5 customers by total order value — a classic aggregation use case.
Setup (in mongosh):
// Create a sample orders collection
const orders = [
{ customer: 'Alice', amount: 120, status: 'completed' },
{ customer: 'Bob', amount: 85, status: 'completed' },
{ customer: 'Alice', amount: 200, status: 'completed' },
{ customer: 'Carol', amount: 45, status: 'pending' },
{ customer: 'Bob', amount: 150, status: 'completed' },
{ customer: 'Dave', amount: 300, status: 'completed' }
];
db.orders.insertMany(orders);
Now, use the aggregation framework to compute total amount per customer, sort by total descending, and limit to top 2 — but only count completed orders.
// Aggregate: top 2 customers by completed order total
db.orders.aggregate([
{ $match: { status: 'completed' } },
{ $group: { _id: '$customer', total: { $sum: '$amount' } } },
{ $sort: { total: -1 } },
{ $limit: 2 }
]);
Expected output:
[
{ _id: 'Alice', total: 320 },
{ _id: 'Bob', total: 235 }
]
Notice how $group calculates totals, then $sort orders by that computed field, and $limit caps the result. This is impossible with a single find() query.
Simple sort and limit without grouping — maybe you just want the 3 most recent products:
// 3 latest products
db.products.aggregate([
{ $sort: { createdAt: -1 } },
{ $limit: 3 }
]);
Expected output (assuming a products collection with a createdAt date field): the 3 documents with the newest createdAt.
Combining with pagination — to implement "page 2" of results, use $skip before $limit:
// Second page of products sorted by price ascending (3 per page)
db.products.aggregate([
{ $sort: { price: 1 } },
{ $skip: 3 },
{ $limit: 3 }
]);
This returns products 4–6 when sorted by price. Always sort before skipping to ensure consistent pagination.
Pro tip: When using
$sortwith$limiton a large collection, ensure that the sort field is indexed. MongoDB can then use the index to avoid an in-memory sort entirely, dramatically improving performance.
Compare options / when to choose what
You have several ways to sort and limit data in MongoDB. Here's a comparison to help you choose:
| Approach | Use case | Pros | Cons |
|---|---|---|---|
find().sort().limit() |
Simple queries, no aggregation | Familiar, easy to read | Cannot sort by computed values, no grouping |
Aggregation $sort + $limit |
Complex transformations, grouping, computed fields | Flexible, powerful, can combine with many stages | Slightly steeper learning curve |
find().skip().limit() |
Basic pagination | Simple | Inconsistent ordering without explicit sort, performance degrades with large skips |
Aggregation $sort + $skip + $limit |
Production pagination | Stable ordering, can be optimized with indexes | Verbose syntax |
When to choose aggregation: Use it when you need to sort by fields that don't exist in the document (e.g., computed totals), group documents before sorting, or chain multiple transformations. The find() method is fine for straightforward queries where you just need to sort by an existing field and limit results.
Troubleshooting & edge cases
Issue: "Exceeded memory limit" error (Sort exceeded memory limit of 104857600 bytes)
MongoDB's in-memory sort is limited to 100 MB. When you sort a large dataset without an index, you'll hit this error. Fix: As a quick solution, allow disk usage with the allowDiskUse option:
db.orders.aggregate(
[ { $sort: { amount: -1 } } ],
{ allowDiskUse: true }
);
But the better fix is to create an index on the sort field:
db.orders.createIndex({ amount: -1 });
This allows MongoDB to stream results from the index without a blocking sort.
Issue: Wrong sort order when using $limit before $sort
If you write $limit before $sort, you're limiting first, then sorting — you get a random subset sorted, not the top N. Always put $sort before $limit.
Issue: $limit: 0 or negative
Passing 0 or a negative number to $limit returns no documents. Use $limit: 1 for a single document or $limit: 0 if you intentionally want an empty result (which is rarely useful).
Issue: Sorting on text or large strings
String sorting uses Unicode order, which may differ from case-insensitive sorts you expect. Use $collation with strength: 2 for case-insensitive sorting:
db.products.aggregate(
[ { $sort: { name: 1 } } ],
{ collation: { locale: 'en', strength: 2 } }
);
Edge case: Multiple sort keys — You can sort by multiple fields, and each field's direction matters. For example, { $sort: { category: 1, price: -1 } } sorts by category ascending, then by price descending within each category. Keep this in mind when you need a secondary sort criterion.
What you learned & what's next
You've mastered using the aggregation framework for sorting and limiting. You learned:
- How
$sortand$limitwork as pipeline stages. - How to combine them with
$matchand$groupfor real-world tasks like top-N queries. - How to paginate with
$skipand$limit. - How to avoid the 100 MB sort memory limit with indexes or
allowDiskUse. - How to fix common issues like incorrect stage order and case sensitivity.
Now you're ready to tackle more advanced aggregation topics. In the next lesson, you'll likely explore window functions with $setWindowFields or learn how to join data across collections with $lookup. Keep practicing — try building a leaderboard from a scores collection or implement pagination for a blog's comments. The aggregation framework is a cornerstone of MongoDB development, and you've just added a powerful tool to your toolkit.
Practice recap
Try this: insert a scores collection with 20 documents (player, score, date). Write an aggregation pipeline that finds the top 3 players by score and sorts ties by date ascending. Experiment with adding an index on score and compare performance with explain(). This will reinforce the concepts of stage order and indexing.
Common mistakes
- Putting
$limitbefore$sort— this limits first and then sorts, giving you a random subset instead of the top N. Always sort before limit. - Forgetting to create an index on the sort field, leading to the 100 MB in-memory sort error on large collections. Use
createIndexcomprehensively. - Using
$skipwithout$sortin pagination — results become inconsistent across pages because the natural order isn't guaranteed. - Passing a negative or zero value to
$limit, which returns an empty result set instead of an error. Always use positive integers.
Variations
- Use
find().sort().limit()for simple queries — it's often faster to write and easier to read when you don't need aggregation stages. - Combine
$sortand$limitwith$projectto reshape the output, like including only certain fields in the final result. - Use aggregation with
$groupand$sorton a computed field to create top-N lists by category (e.g., best-selling products per category).
Real-world use cases
- Leaderboards — compute player scores from a
game_eventscollection, sort descending, and limit to top 10 players. - E-commerce top sellers — aggregate order line items by product, sum quantities, sort, and limit to the top 5 products for a dashboard.
- Activity feeds — paginate recent user actions by sorting on
createdAtdescending and using$skip/$limitto load pages.
Key takeaways
- The aggregation framework's
$sortand$limitstages let you sort by computed fields and cap results inside the database. - Stage order matters — always sort before limit; limit before skip for correct pagination.
- Indexes on sort fields prevent the 100 MB memory limit error and speed up pipelines.
- Combine
$match,$group, and$sortto solve complex top-N queries thatfind()can't handle. - Use
allowDiskUse: trueas a stopgap, but prefer proper indexing for production performance. - The aggregation pipeline is lazy; MongoDB optimizes stage order but your logical order must be correct.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.