mongoui
Performance·June 30, 2026·6 min read

How to speed up a slow MongoDB aggregation pipeline

A slow aggregation is usually a pipeline that scans everything before it filters. The fix is order: put the stages that can use an index first. Here is how to read an aggregation's explain plan and rearrange it.

Arctic Glass illustration for How to speed up a slow MongoDB aggregation pipeline

A slow MongoDB aggregation is almost always a pipeline that does the expensive work before it does the cheap work: it scans the entire collection, then filters down to the fifty documents you wanted. That aggregation has more stages than a touring band, and about the same overnight costs. The fix is rarely a bigger machine. It is order. Move the stages that can use an index to the front, and the pipeline starts from a handful of documents instead of all of them.

Read the pipeline's plan first

Same as a find, an aggregation has an explain() and it tells you where the time goes:

db.orders.aggregate(
  [
    { $match: { status: "shipped" } },
    { $group: { _id: "$userId", total: { $sum: "$amount" } } }
  ],
  { explain: true }
)

Look at the very first stage. An IXSCAN means the pipeline started from an index and only touched matching documents. A COLLSCAN means it read every document in the collection before the $group ran. The aggregation explain output shows the stage breakdown; the first stage is where the win is.

Filter and sort before you transform

Here is the rule that makes aggregations fast: only the stages before the first document-transforming stage can use a collection index. A $match or $sort at the top can hit an index. The same $match after a $group, $unwind, or a $project that changes the shape cannot, because the documents flowing through are computed values the index knows nothing about.

So put $match first, always:

// slow: groups the whole collection, then throws most of it away
[
  { $group: { _id: "$userId", total: { $sum: "$amount" } } },
  { $match: { _id: 123 } }
]

// fast: filters to one user with an index, then groups a tiny set
[
  { $match: { userId: 123 } },
  { $group: { _id: "$userId", total: { $sum: "$amount" } } }
]

Both return the same answer. The second one does a fraction of the work. Give that leading $match an index on userId and the pipeline never scans. Adding that index is creating an index; keeping the right set is index optimization.

Index the join, and mind the sort

Two more places aggregations quietly go slow:

A $lookup is a query per input document against the foreign collection. If the foreign field is not indexed, that is a collection scan repeated thousands of times. Index the foreignField, and the join goes from painful to boring.

A big $sort or $group is capped at 100MB of memory. Blow past it and the stage errors unless you pass allowDiskUse: true to spill to disk. Spilling works, but it is slow. The better fix is to sort on an indexed field early, so the sort rides the index instead of buffering the whole result set in memory.

Or let the optimizer read the plan for you

The reason people avoid aggregation plans is the same reason they avoid query plans: the output is dense, not missing. mongoui's Query Optimizer reads the aggregation's explain() and reports it in plain English, where the scan is, which stage is heavy, and the index that lets the pipeline start from a $match instead of the whole collection. Local narration makes no provider call. Explicit AI narration sends the authored pipeline and bounded structural plan context directly to your selected provider; full documents and MongoDB credentials stay local. This is the aggregation-shaped version of fixing a slow query: filter early, index the first stage, and let the pipeline work on the small set.

Frequently asked questions

How do I make a MongoDB aggregation faster?

Put the stages that can use an index at the front. A $match or $sort as the first stage can use a collection index; the same stage after a $group or a reshaping $project cannot, because the documents no longer match the index. Filter early, then work on the smaller set.

How do I run explain() on an aggregation?

Use db.orders.aggregate(pipeline, { explain: true }), or db.orders.explain("executionStats").aggregate(pipeline). Read the first stage: an IXSCAN means the pipeline started from an index, a COLLSCAN means it read the whole collection before doing anything else.

Does a MongoDB aggregation use indexes?

Only the stages before the first one that transforms the documents. A leading $match and $sort can use an index; once a $group, $unwind, or shape-changing $project runs, later stages operate on computed documents with no index. That is why stage order decides aggregation speed.

What is the 100MB limit in a MongoDB aggregation?

Each pipeline stage is capped at 100MB of memory. A large $sort or $group that exceeds it fails unless you pass allowDiskUse: true, which lets the stage spill to disk. Better still, sort on an indexed field early so the sort never has to buffer the whole set.