Course outline

MongoDB / Intermediate: aggregation and application contracts / Lesson 7

Aggregation stages and grouped reports

Transform documents through explicit stages and preserve the meaning of counts and totals. Separate filtering, grouping, projection and final ordering.

About 4 min readContent reviewed
On this page

Lesson goals

  • Explain pipeline stage inputs and outputs.
  • Validate grouping grain and numeric input types.

Each stage changes a relation of documents

An aggregation pipeline passes documents from one stage to the next. $match filters, $project shapes fields, $group changes grain and $sort establishes output order. Stages do not necessarily preserve one input document per output document. Explain the intended grain after each transformation before combining sums or joins. Most ordinary pipelines are reads, but $out and $merge write to collections; an aggregate method name is therefore not proof that an operation is read-only. Review the complete pipeline before diagnostics or rehearsal.

Group a complete synthetic input

The $documents stage supplies three literal payment documents without creating a persistent collection; it is available from MongoDB 5.1 and used here under an 8.0-compatible baseline. After grouping, customer 7 has total twenty-five and count two, while customer 8 has total five and count one. The final sort makes the output deterministic. These values are reasoned from the literals, not measured from an installed engine. A driver must still send a valid database-level aggregation when using a collectionless first stage.

javascript Illustrative mongosh; results are reasoned, not measured.
db.getSiblingDB('quay_lab').aggregate([
  { $documents: [
    { customerId: 7, amount: 10 }, { customerId: 7, amount: 15 },
    { customerId: 8, amount: 5 }
  ] },
  { $group: { _id: '$customerId', total: { $sum: '$amount' }, count: { $sum: 1 } } },
  { $sort: { _id: 1 } }
]);
Expected output
{ _id: 7, total: 25, count: 2 }
{ _id: 8, total: 5, count: 1 }

The output grain is one document per customer, not one payment.

Filter rows and summaries at different points

A $match before $group decides which source events contribute; a $match after $group selects completed summaries. Filtering individual payments above twenty is not equivalent to keeping customers whose total exceeds twenty. The expression "$amount" is a field path, while "amount" is a literal string in expression contexts. Accumulator behavior also depends on input types: $sum ignores nonnumeric values in its documented contexts, so an apparently plausible total can hide malformed string amounts. Inventory types rather than relying on quiet coercion.

Make a conditional population explicit

The literal input contains paid amounts ten and twenty plus a pending amount five. Filtering paid documents before grouping gives a paid total of thirty and count two. If a dashboard needs paid and pending metrics together, conditional accumulator expressions or a carefully bounded $facet may be appropriate, with separate meanings for each metric. Do not remove the pending population before attempting to count it. Define refunds, cancellations and missing status rules before presenting any aggregate as a financial or service-quality fact.

javascript Illustrative mongosh; results are reasoned, not measured.
db.getSiblingDB('quay_lab').aggregate([
  { $documents: [
    { state: 'paid', amount: 10 }, { state: 'paid', amount: 20 },
    { state: 'pending', amount: 5 }
  ] },
  { $match: { state: 'paid' } },
  { $group: { _id: null, total: { $sum: '$amount' }, count: { $sum: 1 } } }
]);
Expected output
{ _id: null, total: 30, count: 2 }

This fixture has qualifying input. An empty grouped input generally emits no group rather than inventing a zero summary document.

A small output can require large work

Grouping and sorting can process many documents before emitting a tiny report. Early selective $match stages may allow index access, but optimizer transformations and stage ordering should be confirmed with explain rather than assumed. Disk-spill behavior and memory limits depend on stage and version; allowDiskUse is not unlimited memory or a replacement for a bounded model. Reconcile small fixtures by hand, including malformed types, no matches and repeated amounts. Accept performance changes only after confirming the same logical document population and aggregate meaning.

Common errors and fixes

A sum silently excludes numeric-looking text

Why it happens
Nonnumeric inputs did not meet the accumulator’s numeric contract.
How to fix it
Inventory and validate BSON types before computing authoritative totals.
Prevent it next time
Test malformed types instead of only valid numeric fixtures.

A post-group threshold is applied before grouping

Why it happens
Event-level filtering was confused with aggregate selection.
How to fix it
Place the summary predicate after $group.
Prevent it next time
State each stage’s grain and population in review.

A presumed read pipeline writes data

Why it happens
$out or $merge was overlooked.
How to fix it
Inspect the full pipeline and use an isolated approved target for writes.
Prevent it next time
Classify pipelines by actual stages, not method names.

Check your understanding

1. Payments are 10 and 15 for customer 7. Which stage placement answers customers with total above 20?

Show worked solution

Group by customer first and sum amount, then apply $match to the resulting total field. The customer remains with total twenty-five. A pre-group amount > 20 filter removes both payments and answers another question. Include this fixture in tests because a single large payment can make the two incorrect and correct approaches accidentally agree.

2. The paid query has no matching events. Should the application assume it receives {_id:null,total:0}?

Show worked solution

No. A grouping stage over no qualifying input normally emits no group. Define whether the report layer should construct an explicit zero metric, show no data or raise a missing-input warning. That presentation decision must not conflate an empty population with malformed amounts that were ignored by an accumulator. Test both cases separately.

Key takeaways

  • Every pipeline stage has a grain and population contract.
  • Validate types and empty-input behavior before trusting a total.

Official references