How a Pipeline Executes

db.collection.aggregate(pipeline, options) takes an array of stage documents and returns a cursor. Each stage is a single-key document whose key names it. The collection supplies the input stream; every later stage consumes the previous stage's output and nothing else, so a document halfway down may bear no resemblance to what is on disk.

A four-stage pipeline over the orders collection
db.orders.aggregate([
  { $match: { status: 'delivered' } },
  { $unwind: '$items' },
  { $group: { _id: '$items.category',
              revenue: { $sum: { $multiply: ['$items.qty', '$items.price'] } },
              units: { $sum: '$items.qty' } } },
  { $sort: { revenue: -1 } }
])
Output
[
  { _id: 'displays', revenue: 1280, units: 3 },
  { _id: 'audio', revenue: 870, units: 6 },
  { _id: 'keyboards', revenue: 630, units: 6 },
  { _id: 'accessories', revenue: 300, units: 12 }
]

Build it one stage at a time, appending the next only once you have seen the last one's output. $match alone leaves 10 of the 14 orders, still whole. $unwind turns those 10 into 20, one per line item, items now the subdocument that produced the row (Unwinding Arrays). $group collapses the 20 into 4, discarding every field no accumulator captured — no placedAt, no status — and making the group key the new _id. $sort reorders those four.

Documents entering and leaving each stage of the four-stage pipeline
Documents entering and leaving each stage of the four-stage pipeline

The pipeline you write is not the pipeline that runs

The server optimizes your stage array first: merging adjacent stages, turning $sort plus $limit into a bounded top-k sort, dropping unread fields, and moving $match as early as it legally can so the leading stage becomes an indexable query. explain() goes before aggregate().

Explaining a pipeline whose $match sits in the wrong place
db.orders.createIndex({ status: 1, placedAt: -1 })
db.orders.explain().aggregate([
  { $unwind: '$items' },
  { $match: { status: 'delivered' } },
  { $project: { sku: '$items.sku' } }
]).stages.map(s => Object.keys(s)[0])
Output
[ '$cursor', '$unwind', '$project' ]

The $match has vanished: it was folded into $cursor, the leading stage that reads the collection, whose plan is an IXSCAN on status_1_placedAt_-1 examining 10 keys and 10 documents — exactly what writing $match first would give. That is no licence to stop thinking about order: the optimizer moves a $match past a stage only when it can prove the result identical, which it cannot when the match tests a computed field.