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…