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.

Beginner105–130 minutesPredicate operator + regex/index labMongoDB Community Server 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

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.

01

Distinguish any-of membership ($in) from not-in ($nin) and all-required array membership ($all).

02

Use $size for exact array cardinality while recognizing that the $size portion cannot use an index.

03

Use $exists and $type without conflating field presence, explicit null, field type, and array-element type.

04

Use regular expressions deliberately and interpret prefix/index behavior through explain rather than folklore.

05

Recognize when an aggregation expression inside $expr is appropriate and when a simple predicate is clearer and more indexable.

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. $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.”

mongosh · any, not-in, and all-required membership
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.

mongosh · indexed companion predicate plus non-indexable $size portion
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.

mongosh · presence and BSON-type evidence
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.

mongosh · compare anchored prefix and unanchored regex
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.

mongosh · server-side expression predicate (bridge to Lesson 4)
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

shell · start disposable MongoDB on 127.0.0.1:27033
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
shell · operator semantics plus explain evidence
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

  • $in returns categories matching any requested value; $all requires every requested array member.
  • $size:2 returns only arrays of exactly two elements.
  • $exists:true includes 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

  1. How does $all differ from $in on an array field?
  2. Why can $nin match a document where the field is missing?
  3. Can an index satisfy the $size portion directly?
  4. Does $exists:true exclude null?
  5. 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.

bash · cleanup/reset
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

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.