Chapter 04 · Query Operators, Arrays, Nested Fields, Null/Missing Semantics, and Expressions
$in, $nin, $all, $size, $exists, $type, $regex, and Expression-Based Filtering
Use MongoDB predicate operators as precise contracts for membership, cardinality, field presence, BSON type, patterns, and computed filtering.
Learning outcomes
AtlasMart's product-discovery service needs filters for
categories, channels, array membership, array length, optional
fields, mixed legacy types, and product-name patterns. MongoDB
offers concise predicates for each case, but the operators do
not all mean “membership” and they do not all have the same
selectivity or index behavior. This lesson turns
$in, $nin, $all,
$size, $exists, $type,
$regex, and expression-based filtering into precise
contracts.
Distinguish any-of membership ($in) from not-in ($nin) and all-required array membership ($all).
Use $size for exact array cardinality while recognizing that the $size portion cannot use an index.
Use $exists and $type without conflating field presence, explicit null, field type, and array-element type.
Use regular expressions deliberately and interpret prefix/index behavior through explain rather than folklore.
Recognize when an aggregation expression inside $expr is appropriate and when a simple predicate is clearer and more indexable.
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.
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. $in, $nin, and $all answer different membership questions
$in is “equals any value in this list.” If the
stored field is an array, it matches when at least one array
element equals one of the supplied values. $all is
“the stored array contains every required value,” conceptually
similar to an AND over the same array field.
$nin is not merely the complement over present
values: it also matches documents where the field is missing,
which can surprise applications trying to express “present and
not one of these values.”
db.query_products.drop()db.query_products.insertMany([ {_id:"a",category:"camera",channels:["web","store"],status:"active",name:"AtlasCam Mini",rating:4.8,note:null,mixed:[1,"2"],priceCents:12900,listPriceCents:15000}, {_id:"b",category:"audio",channels:["web"],status:"active",name:"AtlasMic USB",rating:"4.6",mixed:[2,3],priceCents:9900,listPriceCents:9000}, {_id:"c",category:"camera",channels:["partner","store"],status:"retired",name:"Pocket Cam",rating:4.1,priceCents:14900,listPriceCents:15000}, {_id:"d",category:"accessory",channels:[],status:"active",name:"Atlas Strap",rating:4.2,note:"clearance",priceCents:2900,listPriceCents:9000}])printjson({ anyCategory: db.query_products.find({category:{$in:["camera","audio"]}},{_id:1}).sort({_id:1}).toArray(), notRetired: db.query_products.find({status:{$nin:["retired"]}},{_id:1}).sort({_id:1}).toArray(), webAndStore: db.query_products.find({channels:{$all:["web","store"]}},{_id:1}).sort({_id:1}).toArray()})
MongoDB's documentation warns that very large
$in parameter arrays can degrade performance
because each parameter must be compared/probed; the docs
recommend keeping parameter counts to the tens where practical
rather than hundreds or more. Treat that as guidance, not a
universal magic threshold: index the field, measure the real
workload, and redesign APIs that routinely send enormous ad-hoc
membership sets.
2. $size is exact cardinality, and the $size portion is not indexable
$size matches an array with exactly the specified
number of elements. It does not accept a range such as “at least
two.” MongoDB's documented behavior is explicit: queries cannot
use indexes for the $size portion, although other
predicates in the same query may still use indexes. If array
cardinality is a frequent range predicate, maintain a validated
counter field rather than forcing every query to inspect array
length.
db.query_products.createIndex({status:1})const q={status:"active",channels:{$size:2}};const e=db.query_products.explain("executionStats").find(q,{_id:1});printjson({matches:db.query_products.find(q,{_id:1}).sort({_id:1}).toArray(), nReturned:e.executionStats.nReturned, totalKeysExamined:e.executionStats.totalKeysExamined, totalDocsExamined:e.executionStats.totalDocsExamined, winningPlan:e.queryPlanner.winningPlan});
Do not conclude “the query uses an index, therefore every
predicate is index-backed.” Explain can show an index narrowing
status while the server still evaluates array
length after fetching candidates.
3. $exists asks about presence; $type asks about BSON type
$exists:true matches documents that contain the
field, including fields whose value is explicitly
null. $exists:false matches missing
fields. $type asks whether a field value has one of
the specified BSON types. For an array-valued field, the query
$type operator can match if at least one array
element has the requested type; use
$type:"array" when you mean the field itself must
be an array.
printjson({ noteExists: db.query_products.find({note:{$exists:true}},{_id:1,note:1}).sort({_id:1}).toArray(), noteMissing: db.query_products.find({note:{$exists:false}},{_id:1}).sort({_id:1}).toArray(), numericRating: db.query_products.find({rating:{$type:"number"}},{_id:1,rating:1}).sort({_id:1}).toArray(), mixedHasString: db.query_products.find({mixed:{$type:"string"}},{_id:1,mixed:1}).sort({_id:1}).toArray(), mixedIsArray: db.query_products.find({mixed:{$type:"array"}},{_id:1}).sort({_id:1}).toArray()})
These operators are useful for migrations and data-quality audits, but a production schema should not rely on every request rediscovering inconsistent types. Validation and schema-version strategies later in the course make the contract explicit.
4. $regex is pattern matching, not a general search engine
$regex performs regular-expression matching on
strings. A case-sensitive prefix expression such as
/^Atlas/ can often use an index to constrain the
candidate range because the literal prefix is known. An
unanchored expression such as /Cam/ cannot derive
the same tight prefix range, and case-insensitive regular
expressions have additional index limitations because
$regex is not collation-aware in the way many
developers assume.
db.query_products.createIndex({name:1})for (const [label,q] of [["prefix",{name:/^Atlas/}],["unanchored",{name:/Cam/}]]) { const e=db.query_products.explain("executionStats").find(q,{_id:1,name:1}); printjson({label,matches:db.query_products.find(q,{_id:1,name:1}).sort({_id:1}).toArray(), nReturned:e.executionStats.nReturned, keys:e.executionStats.totalKeysExamined, docs:e.executionStats.totalDocsExamined, winningPlan:e.queryPlanner.winningPlan});}
The tiny fixture cannot prove production latency. It proves how to gather evidence. For linguistic search, relevance, analyzers, faceting, or typo tolerance, Chapter 21 covers MongoDB Search rather than pretending increasingly complex database regexes are equivalent.
5. Expression-based filtering belongs on the server—but use it only when the condition is computed
A normal query predicate compares a field to a supplied
condition. $expr instead allows aggregation
expressions inside a query predicate, so the server can compare
fields or calculate values without sending every document to the
client. This lesson introduces the boundary; Lesson 4 develops
it fully.
const q={$expr:{$lt:["$priceCents","$listPriceCents"]}};printjson(db.query_products.find(q,{_id:1,priceCents:1,listPriceCents:1}).sort({_id:1}).toArray());
Do not wrap a simple equality such as
status:"active" in a computed expression merely
because $expr exists. Simple query predicates
communicate intent clearly and usually give the optimizer the
most direct indexable form.
AtlasMart operator lab
docker rm -f atlasmart-mongo-ch04-l2docker run --name atlasmart-mongo-ch04-l2 -p 127.0.0.1:27033:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimdocker logs atlasmart-mongo-ch04-l2 --tail 25
mongosh "mongodb://127.0.0.1:27033/atlasmart?directConnection=true" --quiet --eval 'db.query_products.drop();db.query_products.insertMany([ {_id:"a",category:"camera",channels:["web","store"],status:"active",name:"AtlasCam Mini",rating:4.8,note:null,mixed:[1,"2"],priceCents:12900,listPriceCents:15000}, {_id:"b",category:"audio",channels:["web"],status:"active",name:"AtlasMic USB",rating:"4.6",mixed:[2,3],priceCents:9900,listPriceCents:9000}, {_id:"c",category:"camera",channels:["partner","store"],status:"retired",name:"Pocket Cam",rating:4.1,priceCents:14900,listPriceCents:15000}, {_id:"d",category:"accessory",channels:[],status:"active",name:"Atlas Strap",rating:4.2,note:"clearance",priceCents:2900,listPriceCents:9000}]);db.query_products.createIndex({name:1}); db.query_products.createIndex({status:1});const checks={ in:db.query_products.find({category:{$in:["camera","audio"]}},{_id:1}).sort({_id:1}).toArray(), all:db.query_products.find({channels:{$all:["web","store"]}},{_id:1}).sort({_id:1}).toArray(), size:db.query_products.find({channels:{$size:2}},{_id:1}).sort({_id:1}).toArray(), exists:db.query_products.find({note:{$exists:true}},{_id:1,note:1}).sort({_id:1}).toArray(), numeric:db.query_products.find({rating:{$type:"number"}},{_id:1}).sort({_id:1}).toArray(), prefix:db.query_products.find({name:/^Atlas/},{_id:1}).sort({_id:1}).toArray()};printjson(checks);const e=db.query_products.explain("executionStats").find({name:/^Atlas/},{_id:1});printjson({nReturned:e.executionStats.nReturned,keys:e.executionStats.totalKeysExamined,docs:e.executionStats.totalDocsExamined});'
Verification checklist
-
$inreturns categories matching any requested value;$allrequires every requested array member. -
$size:2returns only arrays of exactly two elements. -
$exists:trueincludes the document whose field value is explicit null. -
$type:"number"excludes the legacy string rating. - The anchored regex explain is inspected; no universal latency claim is made from the tiny fixture.
- No operator is presented as a substitute for schema validation or purpose-built search.
Check your understanding
- How does $all differ from $in on an array field?
- Why can $nin match a document where the field is missing?
- Can an index satisfy the $size portion directly?
- Does $exists:true exclude null?
- Why prefer a simple status:"active" predicate over {$expr:{$eq:["$status","active"]}}?
Review the answers
$in requires at least one candidate value to match; $all requires the stored array to contain every specified value.
$nin means the field value is not any prohibited value, and MongoDB also includes documents where the field does not exist.
No. MongoDB documents that the $size portion cannot use an index, although companion predicates may use indexes.
No. It matches field presence, and an explicitly null field is present.
The simple predicate is clearer and exposes a direct query shape that is generally easier to index and optimize.
docker rm -f atlasmart-mongo-ch04-l2
The next lesson focuses on the most common presence bug: explicit null versus a field that is absent.
Authoritative references
- MongoDB release notes — Official current stable server series and patch notes.
- MongoDB 8.3 release notes — Official 8.3 patch history; 8.3.8 is the latest released patch at review time.
- MongoDB query documents — Official find/query behavior and cursor semantics.
- MongoDB query optimization — Official selectivity, index, and explain guidance.
- PyMongo query documents — Official Python driver query-filter behavior.
- $in query operator — Any-of membership semantics and performance guidance for parameter counts.
- $all query operator — All-required array membership semantics.
- $size query operator — Exact array cardinality and index limitation.
- $exists query operator — Field-presence semantics including explicit null.
- $type query operator — BSON type matching and array behavior.
- $regex query operator — Regex syntax, options, and index-performance behavior.