Chapter 04 · Query Operators, Arrays, Nested Fields, Null/Missing Semantics, and Expressions

$expr and Computed Predicates: Comparing Fields and Reusing Aggregation Expressions

Keep computed business predicates on the server with $expr while preserving simple indexable predicates and making missing-value assumptions explicit.

Beginner105–130 minutes$expr computed-invariant labMongoDB Community Server 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning outcomes

AtlasMart's inventory service needs questions that cannot be written as “field compared with constant”: “reserved stock exceeds on-hand stock,” “available stock is below this product's own reorder point,” and “discounted price is lower than list price after handling missing values.” These are computed predicates. MongoDB's $expr lets a find or $match use aggregation expressions so the comparison stays on the server.

01

Use $expr to compare two fields from the same document and to build computed predicates.

02

Reuse aggregation expressions such as $subtract, $ifNull, and comparison operators inside find filters.

03

Explain null/missing behavior in computed expressions and add explicit defaults only when the business semantics justify them.

04

Compare indexability of ordinary field-to-constant predicates with field-to-field/computed expressions using explain evidence.

05

Avoid pulling entire collections to the client merely to evaluate a predicate the server can express.

Chapter 04 reproducible baseline

Mandatory labs use a disposable loopback-only standalone mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim with dedicated AtlasMart query fixtures and explicit reset commands. Driver examples pin pymongo==4.17.0. The standalone is intentionally unauthenticated only for these short-lived local exercises; do not publish it beyond 127.0.0.1. The labs create only local secondary/multikey indexes needed to expose query plans. Read concern, read preference, replication, and sharding are not varied in this chapter because the goal is query semantics and indexability.

Generation-time execution note

Docker, mongod, mongosh, and PyMongo are not available in this generation environment. Commands were checked against current official MongoDB Server and PyMongo documentation, but product commands were not executed here. Expected-output blocks describe stable fields and relationships to verify; they are not fabricated captured transcripts.

1. $expr turns an aggregation expression into a query predicate

$expr has the shape {$expr:{<expression>}}. The inner expression must resolve to a truthy/falsey result for each candidate document. Field paths inside expressions are strings beginning with $, such as "$reserved". This is different from a normal query predicate where the field name itself is the key.

mongosh · field-to-field comparisons with $expr
db.inventory_query.drop()db.inventory_query.insertMany([ {_id:"sku-a",status:"active",onHand:10,reserved:4,reorderPoint:3,listPriceCents:15000,promoPriceCents:12900}, {_id:"sku-b",status:"active",onHand:5,reserved:7,reorderPoint:2,listPriceCents:9000,promoPriceCents:9500}, {_id:"sku-c",status:"active",onHand:8,reserved:6,reorderPoint:4,listPriceCents:12000}, {_id:"sku-d",status:"retired",onHand:100,reserved:0,reorderPoint:10,listPriceCents:5000,promoPriceCents:4000}])printjson({ overReserved: db.inventory_query.find({$expr:{$gt:["$reserved","$onHand"]}},{_id:1,onHand:1,reserved:1}).sort({_id:1}).toArray(), promoBelowList: db.inventory_query.find({$expr:{$lt:["$promoPriceCents","$listPriceCents"]}},{_id:1,listPriceCents:1,promoPriceCents:1}).sort({_id:1}).toArray()})

sku-b is over-reserved because its own two fields violate the invariant. The predicate does not need an application loop. Note that a missing promoPriceCents introduces missing/null expression behavior; do not assume every arithmetic or comparison expression automatically means what your business wants for absent data.

2. Build the business expression once and reuse it

Available stock is onHand - reserved. AtlasMart wants active products where available stock falls below each product's own reorderPoint. The reusable expression can be placed in a find filter and then reused in an aggregation pipeline that also projects the computed value for diagnosis.

mongosh · reuse one computed predicate in find and aggregation
const available={$subtract:["$onHand","$reserved"]};const needsReorder={$lt:[available,"$reorderPoint"]};printjson(db.inventory_query.find({status:"active",$expr:needsReorder},{_id:1,onHand:1,reserved:1,reorderPoint:1}).sort({_id:1}).toArray());printjson(db.inventory_query.aggregate([ {$match:{status:"active",$expr:needsReorder}}, {$project:{_id:1,onHand:1,reserved:1,reorderPoint:1,available}}, {$sort:{_id:1}}]).toArray());

Keeping the ordinary status:"active" predicate outside $expr is intentional. It is simpler, easier to read, and directly indexable. Use expressions for the part that truly requires per-document computation.

3. Missing values require explicit business semantics, not accidental coercion

If old AtlasMart records omit reserved, does that mean zero reserved, unknown reserved, or invalid data? Only the domain can answer. If the contract explicitly says “missing reserved in legacy rows means zero,” $ifNull can encode that default because it treats null/missing as the fallback case. If missing means “unknown,” defaulting to zero would fabricate inventory.

mongosh · explicit legacy default with $ifNull
db.inventory_query.insertOne({_id:"sku-legacy",status:"active",onHand:3,reorderPoint:5})const availableLegacy={ $subtract:[{$ifNull:["$onHand",0]},{$ifNull:["$reserved",0]}]};const q={status:"active",$expr:{$lt:[availableLegacy,"$reorderPoint"]}};printjson(db.inventory_query.find(q,{_id:1,onHand:1,reserved:1,reorderPoint:1}).sort({_id:1}).toArray())

Document the fallback in the schema/version migration. Expressions can make a query resilient, but they should not hide unbounded data-quality debt.

4. $expr is powerful, but computed predicates can be less index-friendly

An ordinary predicate such as {status:"active",onHand:{$lt:5}} compares indexed fields with constants and is a classic indexable shape. A field-to-field predicate such as {$expr:{$gt:["$reserved","$onHand"]}} depends on two values from each document; an ordinary B-tree index cannot precompute every such relationship. MongoDB can optimize some $expr comparisons involving a field and a constant, especially in documented $lookup contexts, but do not extrapolate that to arbitrary field-to-field arithmetic. Multikey indexes are also not supported by $expr.

mongosh · compare direct and computed query shapes
db.inventory_query.createIndex({status:1,onHand:1})const simple={status:"active",onHand:{$lt:5}};const computed={status:"active",$expr:{$gt:["$reserved","$onHand"]}};for (const [label,q] of [["simple",simple],["computed",computed]]) { const e=db.inventory_query.explain("executionStats").find(q,{_id:1}); printjson({label,nReturned:e.executionStats.nReturned,keys:e.executionStats.totalKeysExamined,  docs:e.executionStats.totalDocsExamined,winningPlan:e.queryPlanner.winningPlan});}

If a computed predicate becomes a hot path, consider whether the application should maintain a derived field such as available or needsReorder under a well-defined write invariant and index that field. That trades write complexity for read efficiency; it is not automatically better.

5. AtlasMart computed-predicate lab

shell · start disposable MongoDB on 127.0.0.1:27035
docker rm -f atlasmart-mongo-ch04-l4docker run --name atlasmart-mongo-ch04-l4 -p 127.0.0.1:27035:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimdocker logs atlasmart-mongo-ch04-l4 --tail 25
shell · server-side computation plus explain comparison
mongosh "mongodb://127.0.0.1:27035/atlasmart?directConnection=true" --quiet --eval 'db.inventory_query.drop();db.inventory_query.insertMany([ {_id:"sku-a",status:"active",onHand:10,reserved:4,reorderPoint:3}, {_id:"sku-b",status:"active",onHand:5,reserved:7,reorderPoint:2}, {_id:"sku-c",status:"active",onHand:8,reserved:6,reorderPoint:4}, {_id:"sku-legacy",status:"active",onHand:3,reorderPoint:5}, {_id:"sku-d",status:"retired",onHand:100,reserved:0,reorderPoint:10}]);db.inventory_query.createIndex({status:1,onHand:1});const available={$subtract:[{$ifNull:["$onHand",0]},{$ifNull:["$reserved",0]}]};const q={status:"active",$expr:{$lt:[available,"$reorderPoint"]}};printjson(db.inventory_query.aggregate([{$match:q},{$project:{_id:1,available,reorderPoint:1}},{$sort:{_id:1}}]).toArray());for (const [label,filter] of [["simple",{status:"active",onHand:{$lt:5}}],["computed",q]]) { const e=db.inventory_query.explain("executionStats").find(filter,{_id:1}); printjson({label,nReturned:e.executionStats.nReturned,keys:e.executionStats.totalKeysExamined,docs:e.executionStats.totalDocsExamined});}' 

Verification checklist

  • The field-to-field over-reservation predicate identifies the intentionally invalid record.
  • The reusable available-stock expression is used in both matching and projection.
  • The legacy default is explicit through $ifNull; the lesson states that this is a domain choice, not a universal rule.
  • The direct and computed predicates are both explained, with keys/docs examined captured from the local environment.
  • The ordinary equality/range predicates stay outside $expr where possible.
  • No client-side full-collection loop is needed to decide the server-side condition.

Check your understanding

  1. What does $expr add to find queries?
  2. Why keep status:"active" outside $expr?
  3. Is $ifNull always the correct way to handle missing numeric fields?
  4. Why can field-to-field $expr comparisons be harder to index?
  5. What is one redesign if a computed predicate becomes a critical hot path?
Review the answers

It allows aggregation expressions to be evaluated as query predicates, including comparisons between fields and computed values.

It is already expressible as a normal query predicate, which is clearer and exposes a direct indexable query shape.

No. It is correct only when the business contract says null/missing should map to that fallback value.

The relationship depends on values from the same document rather than a field compared with a fixed constant that a B-tree can directly bound.

Maintain a validated derived field under the write contract and index it, after measuring whether the extra write/storage complexity is justified.

bash · cleanup/reset
docker rm -f atlasmart-mongo-ch04-l4

The final lesson turns the chapter into a query-review discipline for expensive or accidentally non-indexable predicates.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.