iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Expressions / $expr

Aggregation expressions are the mini-language inside $project, $match, $group, and the $expr operator. They look like JSON ({ $eq: [...] }), can reach into nested fields with $., and let you compute new values, compare across fields, and run conditional logic without leaving the query layer.

Computed fields, cross-field filters, and conditionals

EXAMPLE
// 1) $expr inside a find lets you compare two fields of the same document.
//    Without $expr there is no way to say WHERE expires_at < created_at.
await db.collection('orders').find({
  $expr: {
    $and: [
      { $gt: ['$total_cents', '$shipping_cents'] },
      { $eq: ['$currency', 'AUD'] },
    ],
  },
}).toArray();

// 2) Computed fields in $project — pre-shape the document for the client
const pipeline = [
  { $project: {
      _id: 0,
      id:       '$_id',
      customer: 1,
      total:    { $divide: ['$total_cents', 100] },
      // $cond ? : a la ternary — old syntax
      tier:     { $cond: [{ $gte: ['$total_cents', 50000] }, 'gold', 'silver'] },
      // $switch — multi-way branching with a default
      bucket:   { $switch: {
        branches: [
          { case: { $lt: ['$total_cents',  5000] }, then: 'tiny'   },
          { case: { $lt: ['$total_cents', 20000] }, then: 'small'  },
          { case: { $lt: ['$total_cents', 50000] }, then: 'medium' },
        ],
        default: 'large',
      } },
      // Concatenate strings — handy for display names and slugs
      label: { $concat: ['#', { $toString: '$order_no' }, ' / ', '$customer'] },
      // Null-safe defaults
      notes: { $ifNull: ['$notes', ''] },
  } },
];

// 3) Array expressions — $map, $filter, $reduce work on array fields
const withTotals = [
  { $project: {
      // Sum line item subtotals computed on the fly
      items_total: {
        $reduce: {
          input: '$items',
          initialValue: 0,
          in: { $add: ['$$value', { $multiply: ['$$this.qty', '$$this.price_cents'] }] },
        },
      },
      // Transform each line item — quantize price to dollars
      items: {
        $map: {
          input: '$items',
          as: 'i',
          in: {
            sku: '$$i.sku',
            qty: '$$i.qty',
            price: { $divide: ['$$i.price_cents', 100] },
          },
        },
      },
  } },
];

// 4) Date arithmetic for time-series rollups
const byHour = [
  { $group: {
      _id: { hour: { $dateTrunc: { date: '$created_at', unit: 'hour' } } },
      orders: { $sum: 1 },
      revenue: { $sum: '$total_cents' },
  } },
  { $sort: { '_id.hour': 1 } },
];

// 5) Index-friendly $expr matches use $eq to a constant where possible.
//    Cross-field comparisons cannot use an index — keep them after a
//    $match that narrows the working set first.

Why it matters

Reach for $expr only when the comparison crosses fields or needs a computed value. Plain {field: value} matches stay index-eligible; the moment you wrap a predicate in $expr, the planner has to evaluate it row by row. Order pipelines so cheap, indexable matches run first.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// $expr lets you compare fields in a find()
db.orders.find({ $expr: { $gt: ['$total', '$budget'] } });
Try it Yourself »

Discussion

Loading…