What does $expr allow inside a $match stage that ordinary query operators cannot do?
answer
- query predicates compare against constants only
- the right-hand side cannot be another field
- opens the pipeline's operator language in a filter
- operators take an argument array here
- no index can relate two fields of one document
basics
~20 s$expr embeds aggregation expressions in a query filter, so a $match can compare two fields of the same document — { $expr: { $gt: ["$spent", "$budget"] } } — or apply expression operators such as $dateTrunc, which plain query operators cannot express.
solid answer
~50 sOrdinary query operators compare a field against a **constant**: `{ spent: { $gt: 500 } }` works, but `{ spent: { $gt: "$budget" } }` does not — the right-hand side is taken as the literal string `"$budget"`. `$expr` opens the aggregation expression language inside the filter, where operators take an argument array and both sides may be field paths: `{ $expr: { $gt: ["$spent", "$budget"] } }`. That makes same-document field comparisons possible, and lets you compute inside the predicate with `$add`, `$dateTrunc`, `$cond`, `$size` and the rest. It works in `$match` and in `find()` filters. Two caveats: `$expr` does not support multikey indexes, and a comparison between two fields of the same document cannot be answered by an index at all, so it is evaluated per document. Aggregation expressions also lack the query language's implicit array traversal — `{ $eq: ["$tags", "x"] }` compares the whole array, unlike `{ tags: "x" }`.
code
javascript · 10 lines// query language: right-hand side is always a literal
db.projects.find({ spent: { $gt: 500 } })
// expression form: both sides may be field paths
db.projects.find({ $expr: { $gt: ["$spent", "$budget"] } })
// same predicate as a pipeline stage, narrowed by an indexed field first
db.projects.aggregate([
{ $match: { status: "open", $expr: { $gt: ["$spent", "$budget"] } } }
])go deeper
Recognise the shape: $expr wraps an aggregation expression such as { $gt: ["$a", "$b"] } so a filter can compare two fields of the same document. Know that a plain predicate cannot do that.
Explain why the two languages differ — query predicates compare a field against a value, expressions take argument arrays — and demonstrate computing inside a predicate with operators like $size or $dateTrunc.
Show the performance judgment: a same-document field comparison cannot use an index, so pair it with a selective indexed predicate, verify with explain(), and know when to store the derived value instead.
Own the pattern-level call: repeated cross-field filters in hot paths are a modelling signal. Decide whether a maintained derived field, with the write-path cost that implies, beats expressive read-time predicates.
## Two languages in one product MongoDB has two distinct expression languages. The **query language** is what you pass to `find()` and to `$match`: a document of field-to-predicate pairs, where `{ status: "open", qty: { $gte: 10 } }` means "status equals open AND qty is at least 10". The **aggregation expression language** is what you write inside `$project`, `$group` and friends: prefix-form operators over argument arrays, such as `{ $gte: ["$qty", 10] }`, where any argument may be a field path, a literal, or another expression. The query language is deliberately restricted, which is what makes it index-friendly, but the restriction bites: the right-hand side of a query predicate is always a value, never a reference to another field. `$expr` is the bridge that lets you drop an aggregation expression into a query context. ## The canonical use: comparing two fields "Find every project where spend exceeded budget" has no query-language spelling. Written the natural way, `{ spent: { $gt: "$budget" } }`, MongoDB compares `spent` to the eight-character string `"$budget"` — a comparison that matches nothing useful and raises no error. The correct form is: ```javascript db.projects.find({ $expr: { $gt: ["$spent", "$budget"] } }) ``` The same document works as a pipeline stage: `{ $match: { $expr: { $gt: ["$spent", "$budget"] } } }`. ## Computing inside the predicate Because the whole expression language is available, you can filter on things you would otherwise have to compute in an earlier `$set` stage: - `{ $expr: { $gt: [{ $size: "$items" }, 3] } }` — more than three line items. (The query-language `$size` operator only tests an exact length, so this is a genuine capability gain.) - `{ $expr: { $eq: [{ $dateTrunc: { date: "$createdAt", unit: "day" } }, ISODate("2024-05-01")] } }` — created on a particular day, without storing a separate date field. - `{ $expr: { $gt: ["$revenue", { $multiply: ["$cost", 1.2] }] } }` — margin above twenty percent. It also appears in schema-validation documents, where `$jsonSchema` cannot express a relationship between two fields but `$expr` can — for example requiring that `endDate` be later than `startDate`. ## Indexes and cost This is where candidates separate themselves. `$expr` is not automatically slow, but it is not automatically fast either: - A comparison **between two fields of the same document** can never be served by an index, because no single index key encodes the relationship. Every candidate document must be fetched and evaluated. If the collection is large, pair the `$expr` with an ordinary indexed predicate that narrows the set first: `{ $match: { status: "open", $expr: { $gt: ["$spent", "$budget"] } } }` lets the index on `status` do the narrowing. - `$expr` **does not support multikey indexes**, so a predicate over an array field will not get index help from one. The honest summary for an interview: treat `$expr` as an expressive filter that usually runs as a scan over whatever the rest of the predicate lets through, and verify with `explain()` rather than assuming. ## Array semantics differ, and this trips people The query language traverses arrays implicitly: `{ tags: "x" }` matches a document whose `tags` array *contains* `"x"`. Aggregation expressions do not. `{ $expr: { $eq: ["$tags", "x"] } }` compares the entire array value to the string and matches nothing. The aggregation-side equivalent is `{ $expr: { $in: ["x", "$tags"] } }` — note the argument order, value first, array second, the reverse of the query operator `$in`. Similarly, `{ "items.sku": "A1" }` in query language reaches into an array of sub-documents, while an expression path `"$items.sku"` evaluates to an *array of skus*. This asymmetry is the single most common `$expr` bug: a filter that silently matches nothing after someone converted a working query predicate into expression form. ## When not to use it If a plain query predicate expresses what you need, use it — it is more index-friendly and easier to read. `$expr` earns its place when the predicate genuinely relates two parts of the same document or requires computation. An alternative, when the computed value is needed more than once, is to compute it in a `$set` stage and match on the result in the next stage; in a pipeline that is often clearer, though it does not help the index situation either. If a field comparison drives a hot query path, the durable fix is usually to **store the derived value** — a boolean `overBudget` maintained on write — and index that instead.
- Why does { $expr: { $eq: ["$tags", "x"] } } fail to match a document whose tags array contains "x"?Aggregation expressions have no implicit array traversal. The path `"$tags"` evaluates to the whole array, and an array is not equal to the string. The query language's `{ tags: "x" }` does traverse. The expression-side equivalent is `{ $expr: { $in: ["x", "$tags"] } }`, whose argument order is value first, array second.
- How would you keep a two-field comparison from scanning a large collection?Combine it with an ordinary indexed predicate in the same `$match` so the index narrows the candidate set before the expression is evaluated per document, and confirm with `explain()`. If the comparison drives a hot path, store the derived answer — a boolean maintained on write — and index that instead of computing it at read time.
- Can $expr be used outside the aggregation pipeline?Yes. `$expr` is a query operator, so it is valid in `find()` filters, in the filter of an update or delete, and inside collection validation rules, where it expresses cross-field constraints that `$jsonSchema` cannot — such as requiring endDate to be later than startDate.
saying these in an interview costs you the question
- Writes { spent: { $gt: "$budget" } } and expects a field comparison
- Thinks $expr always uses an index like a normal predicate
- Assumes aggregation expressions traverse arrays like query predicates
- Uses $expr for simple constant comparisons out of habit
- Mixes up the argument order of the aggregation $in