Filter concepts by levelShowing all levels.

MongoDB · Section 5

Query Language

Level
intermediate
Read
30 min
Concepts
8

The three basic filter shapes, the comparison/logical/element/array/evaluation operator families, dot notation for nested fields and array elements, and how compound query predicates interact with compound indexes via the prefix rule.

What is true here

  1. { field: value } is equality by default; range and every other comparison need an explicit operator.
  2. $and/$or/$nor take a list of condition documents; $not is different — it wraps one operator to invert it.
  3. { field: null } matches both "absent" and "present but null" — $exists is the operator that tells them apart.
  4. $elemMatch requires several conditions to hold for the same array element, not just the array as a whole.
  5. A compound index only serves queries matching its leading field(s), in the order the index was created — the prefix rule.

What you will be able to do

  • Write equality, range and array-field queries without extra operators where none are needed
  • Choose $and/$or/$nor/$not correctly, including when explicit $and is actually required
  • Use $exists and $type to audit a schema-flexible collection for missing or drifted fields
  • Use $elemMatch to avoid the cross-element false-match mistake on arrays of embedded documents
  • Predict whether a given query can use a compound index, from its field order and the prefix rule

The operator families

The basic filter shapes, then comparison, logical, and element operators — the everyday query vocabulary.

Equality, range and array queries

corebeginner

A filter document is equality by default: { status: "shipped" } means exactly that value. Range queries add an operator like $gte. Querying a field that holds an array matches if any element matches.

Think of it as

Every filter key is a field name; every filter value is either a literal (equality) or an operator document like { $gte: 10 } (range). Nothing marks a field as "an array field" in the query — MongoDB just tries the match against the whole array first, then against each element, so the same { tags: "sale" } filter form works whether tags is a scalar or an array.

text
db.<collection>.find({ <field>: <value> | { <operator>: <value> } })

What we're doing: Show that a scalar filter against an array field matches on "any element," which surprises readers expecting exact-value semantics.

array-field-query.txttext
db.products.insertOne({ name: "Tee", tags: ["clothing", "sale", "summer"] })

db.products.find({ tags: "sale" })
// -> matches the document above, even though tags is a 3-element array, not the scalar "sale"
1
tags is an array field with three string elements.
4
A scalar equality filter on an array field matches if any element equals it — no $elemMatch or index needed for this simple case.

Why this works: This "any element" behavior is what makes tag-style array fields queryable without extra syntax — but it also means { tags: "sale" } and { tags: ["sale"] } mean different things: the first matches "sale" anywhere in the array, the second requires the array to be exactly that one-element array.

Expecting { field: [x] } to mean "the array contains x"

Wrong

text
db.products.find({ tags: ["sale"] })
// intended "contains sale" — actually requires tags to be EXACTLY ["sale"], nothing else

Better

text
db.products.find({ tags: "sale" })
// or, to be explicit: db.products.find({ tags: { $in: ["sale"] } })

What you see: A query that should match several documents with varied tag arrays returns nothing, or only documents with that exact single-element array.

Why: A filter value that is itself an array is compared for exact array equality, not treated as "contains." Passing a bare scalar is what triggers the any-element match.

Same filter shape, three matching rules

equality

{ status: "shipped" }

range

{ price: { $gte, $lt } }

array

matches any element

  1. equality — { status: "shipped" }
  2. range — { price: { $gte, $lt } }
  3. array — matches any element

The three basic query shapes

The three basic query shapes
ShapeFilterMatches
Equality{ status: "shipped" }status is exactly "shipped"
Range{ price: { $gte: 10, $lt: 50 } }price is 10 to just under 50
Array, scalar match{ tags: "sale" }tags array contains "sale"
Array, exact match{ tags: ["sale", "new"] }tags is exactly this array, in this order

Together

text
db.products.find({ price: { $gte: 10, $lt: 50 } })
db.products.find({ tags: "sale" })              // matches if "sale" is anywhere in the tags array
db.products.find({ tags: ["sale", "new"] })      // matches only an exact two-element array, in this order

Remember: Equality is a bare value, range is an operator document, and a scalar filter on an array field matches if any element matches.

See also: comparison operators · array operators

Comparison operators

corebeginner

Eight operators compare a field to a value or a list of values: $eq/$ne for equal/not-equal, $gt/$gte/$lt/$lte for ranges, and $in/$nin for "is one of" / "is none of" a list.

Think of it as

$eq is what a bare { field: value } already means, so it is rarely written explicitly — the other seven exist because there is no bare-value syntax for "not equal," "greater than," or "one of several values." $in is the array-friendly OR: db.x.find({ status: { $in: ["a","b"] } }) reads as "status is a or b" without an explicit $or.

text
db.<collection>.find({ <field>: { <$eq|$ne|$gt|$gte|$lt|$lte|$in|$nin>: <value or array> } })

What we're doing: Show $in replacing a longer $or, and that $in and $nin take an array, not a bare value.

in-vs-or.txttext
// Equivalent to: db.orders.find({ $or: [ { status: "pending" }, { status: "shipped" } ] })
db.orders.find({ status: { $in: ["pending", "shipped"] } })

db.orders.find({ status: { $nin: ["cancelled", "refunded"] } })
// -> matches every status except the two listed
1
$in on one field is a shorter, equivalent form of an $or across repeated equality checks on that same field.
4
$nin is the negation — matches documents where the field is none of the listed values.

Why this works: $in reads more directly than an equivalent $or once there are three or more alternatives, and it is the natural fit when the list of values is already an array from elsewhere in the code (e.g. a set of selected checkboxes).

Passing a bare value to $in instead of an array

Wrong

text
db.orders.find({ status: { $in: "pending" } })
// throws — $in requires an array argument

Better

text
db.orders.find({ status: { $in: ["pending"] } })

What you see: MongoDB rejects the query with an error naming $in and requiring an array, even for what feels like a single-value case.

Why: $in and $nin are defined to take an array of candidates, even when there is only one — there is no single-value shorthand the way { field: value } is shorthand for $eq.

Equality, range, and membership

$eq / $ne

equal / not equal

$gt/$gte/$lt/$lte

range comparisons

$in / $nin

matches any / none of a list

  • $eq / $ne — equal / not equal
  • $gt/$gte/$lt/$lte — range comparisons
  • $in / $nin — matches any / none of a list

The eight comparison operators

The eight comparison operators
OperatorMeaningExample
$eqequal to{ status: { $eq: "shipped" } }
$nenot equal to{ status: { $ne: "cancelled" } }
$gtgreater than{ price: { $gt: 100 } }
$gtegreater than or equal{ price: { $gte: 100 } }
$ltless than{ price: { $lt: 100 } }
$lteless than or equal{ price: { $lte: 100 } }
$inmatches any value in the array{ status: { $in: ["pending", "shipped"] } }
$ninmatches none of the values in the array{ status: { $nin: ["cancelled", "refunded"] } }

Together

text
db.orders.find({ price: { $gte: 20, $lte: 100 } })
db.orders.find({ status: { $in: ["pending", "shipped"] } })
db.orders.find({ status: { $nin: ["cancelled", "refunded"] } })

Remember: $eq/$ne, $gt/$gte/$lt/$lte, $in/$nin — $in/$nin always take an array, even for one value.

See also: equality range and array queries · logical operators

Logical operators: $and, $or, $nor, $not

corebeginner

$and and $or combine a list of condition documents; $nor matches documents that fail every listed condition. $not is different — it wraps a single operator to invert it, not a list of conditions.

Think of it as

A plain filter object is already an implicit $and — { a: 1, b: 2 } means a is 1 AND b is 2. Explicit $and is only needed when the same field appears with two different operators that cannot both live in one operator document, or to nest logic. $or and $nor take that same array-of-conditions shape; $not stands apart, wrapping one operator expression rather than a list.

text
db.<collection>.find({ $and: [ <filter>, ... ] })
db.<collection>.find({ $or: [ <filter>, ... ] })
db.<collection>.find({ field: { $not: <operator expression> } })

What we're doing: Show why $and is sometimes required even though a plain object is already an implicit AND.

explicit-and.txttext
// WRONG: a JS object cannot repeat the key "price" twice
// db.products.find({ price: { $gt: 10 }, price: { $lt: 50 } })

db.products.find({ $and: [ { price: { $gt: 10 } }, { price: { $lt: 50 } } ] })
// -> both conditions on price are kept, because each lives in its own list entry
2
A plain object cannot express two separate operator conditions on the same field — the second "price" key would silently overwrite the first.
5
Explicit $and gives each condition its own object, so repeated fields with different operators do not collide.

Why this works: MongoDB actually allows { price: { $gt: 10, $lt: 50 } } as one combined operator document for this specific case (both operators on one field), so $and is not strictly needed here — but it becomes necessary the moment the same operator needs to appear twice, or the conditions need to nest with $or.

Writing the same key twice in one filter object, expecting both conditions to apply

Wrong

text
// { status: "pending", status: { $ne: "cancelled" } } — second "status" silently wins in most JS/JSON, first is lost

Better

text
db.orders.find({ $and: [ { status: "pending" }, { status: { $ne: "cancelled" } } ] })

What you see: A filter meant to combine two conditions on the same field behaves as if only one of them was ever written.

Why: A JS/JSON object cannot hold two entries with the same key — the second silently overwrites the first before the query is even sent. $and sidesteps this by giving each condition its own list entry.

Implicit AND, explicit everything else

{ a, b }

implicit $and

$or / $nor

array of conditions

$not

inverts one operator

  1. { a, b } — implicit $and
  2. $or / $nor — array of conditions
  3. $not — inverts one operator

The four logical operators

The four logical operators
OperatorShapeMatches
$and{ $and: [ {...}, {...} ] }every listed condition is true
$or{ $or: [ {...}, {...} ] }at least one listed condition is true
$nor{ $nor: [ {...}, {...} ] }none of the listed conditions is true
$not{ field: { $not: { $gt: 10 } } }the wrapped condition is false

Together

text
db.orders.find({ $or: [ { status: "pending" }, { total: { $gt: 500 } } ] })
db.orders.find({ $nor: [ { status: "cancelled" }, { status: "refunded" } ] })
db.orders.find({ total: { $not: { $gt: 500 } } })

Remember: A plain filter is already $and. $or/$nor take a list of conditions; $not wraps one operator to invert it.

See also: comparison operators · dot notation for nested fields

Element operators: $exists, $type

standardbeginner

$exists tests whether a field is present at all, regardless of its value. $type tests what BSON type a field holds. Both matter in a schema-flexible database, where two documents in the same collection can differ on either.

Think of it as

Neither operator cares about the field's value — $exists asks "is this key even in the document," $type asks "what kind of value is it, if present." They are the tools for the shape-drift questions a fixed relational schema would answer for free: "does every document have this field," "did anyone ever store a string where a number was expected."

text
db.<collection>.find({ <field>: { $exists: <boolean> } })
db.<collection>.find({ <field>: { $type: <"alias" | code | [alias, ...]> } })

What we're doing: Show $type catching a schema-flexibility bug: some documents stored price as a string instead of a number.

type-drift.txttext
db.products.insertMany([
  { name: "Mug", price: 12.99 },
  { name: "Pen", price: "9.99" },  // inserted by a buggy import script — a string, not a number
])

db.products.find({ price: { $type: "string" } })
// -> finds the "Pen" document, the one with the wrong type
1
A schema-flexible collection allows this without any error — both documents insert successfully.
4
$type: "string" finds exactly the documents where price accidentally became a string.

Why this works: Because MongoDB does not enforce a field's type by default, a bug in an import script or a client library can silently store the "wrong" BSON type in some documents. $type is the query-time tool for auditing a collection for that kind of drift.

Using { field: null } to test for a missing field

Wrong

text
db.products.find({ discount: null })
// also matches documents where discount really is stored as null

Better

text
db.products.find({ discount: { $exists: false } })

What you see: A query meant to find documents missing a field also returns documents that store that field with an explicit null value.

Why: { field: null } matches both "absent" and "present but null" — the two cases $exists is built to tell apart. This is the same mistake flagged in the equality/range/array concept, restated here because $exists is its actual fix.

$exists and $type

$exists and $type
OperatorQueryMatches
$exists{ discount: { $exists: true } }discount field is present (any value, incl. null)
$exists{ discount: { $exists: false } }discount field is entirely absent
$type{ price: { $type: "double" } }price holds a BSON double
$type{ price: { $type: ["double", "int"] } }price holds either type

Together

text
db.products.find({ discount: { $exists: true } })
db.products.find({ discount: { $exists: false } })
db.products.find({ price: { $type: "string" } })   // finds the type-drift bugs

Remember: $exists checks presence, not value — { f: null } is not the same as $exists: false. $type checks BSON type, string alias or numeric code.

See also: equality range and array queries · bson types

Advertisement

Arrays, paths, and performance

Matching within arrays correctly, reaching into nested fields, pattern matching, and what actually makes a compound index fast.

Array operators: $all, $elemMatch, $size

standardintermediate

$all matches an array containing every listed value, in any order. $elemMatch matches when one single array element satisfies several conditions at once. $size matches by exact array length.

Think of it as

A bare scalar filter on an array field already matches "any element equals this" — $all extends that to "every one of these values is present somewhere in the array" (still no single-element requirement). $elemMatch is the odd one out: it exists because independent conditions on an array of documents can each be satisfied by a *different* element, which is usually not what was meant — $elemMatch forces all of them onto the same element.

text
db.<collection>.find({ <arrayField>: { $all: [<v1>, ...] } })
db.<collection>.find({ <arrayField>: { $elemMatch: { <cond1>, <cond2>, ... } } })
db.<collection>.find({ <arrayField>: { $size: <n> } })

What we're doing: Show $elemMatch preventing a cross-element false match on an array of embedded documents.

elemmatch-vs-separate-conditions.txttext
db.students.insertOne({
  name: "Alex",
  results: [ { subject: "math", score: 60 }, { subject: "art", score: 95 } ],
})

// WRONG: matches Alex even though no single result is both math AND >= 90
db.students.find({ "results.subject": "math", "results.score": { $gte: 90 } })

// RIGHT: requires one element to satisfy both conditions
db.students.find({ results: { $elemMatch: { subject: "math", score: { $gte: 90 } } } })
6
Two separate dot-notation conditions can each be satisfied by a different array element — math from one, >= 90 from another.
9
$elemMatch requires both conditions to hold for the same element, correctly excluding Alex.

Why this works: Dot-notation conditions on an array of embedded documents are evaluated independently against the whole array, not against "the same element" — this is the single most common array-query mistake, and $elemMatch is the direct fix for it.

Writing separate dot-notation conditions expecting them to apply to one array element

Wrong

text
db.students.find({ "results.subject": "math", "results.score": { $gte: 90 } })

Better

text
db.students.find({ results: { $elemMatch: { subject: "math", score: { $gte: 90 } } } })

What you see: The query returns documents where no single array element actually satisfies every condition — the conditions are quietly matching across different elements.

Why: MongoDB treats each dot-notation condition on an array-of-documents field as its own independent test against the array; only $elemMatch groups multiple conditions onto one shared element.

$elemMatch forces conditions onto one element

Without $elemMatch

{ score: 95, grade: "B" }

satisfies score >= 90

{ score: 60, grade: "A" }

satisfies grade == "A" — different element!

With $elemMatch

one element only

must satisfy score >= 90 AND grade == "A" together

  • Without $elemMatch
    • { score: 95, grade: "B" } — satisfies score >= 90
    • { score: 60, grade: "A" } — satisfies grade == "A" — different element!
  • With $elemMatch
    • one element only — must satisfy score >= 90 AND grade == "A" together

The three array operators

The three array operators
OperatorQueryMatches
$all{ tags: { $all: ["sale", "summer"] } }array contains both values
$elemMatch{ scores: { $elemMatch: { $gt: 80, $lt: 90 } } }one element satisfies both bounds
$size{ tags: { $size: 3 } }array has exactly 3 elements

Together

text
db.products.find({ tags: { $all: ["sale", "summer"] } })
db.scores.find({ results: { $elemMatch: { subject: "math", score: { $gte: 90 } } } })
db.products.find({ tags: { $size: 3 } })

Remember: $all: contains every listed value. $elemMatch: one element satisfies every condition. $size: exact length only, no range.

See also: equality range and array queries · arrays of scalars and documents

Evaluation operators and regular expressions

standardintermediate

Evaluation operators run a computation while matching: $regex for patterns, $mod for modulo, $expr for aggregation-style expressions, $text for full-text search. Only an anchored $regex ("^prefix") can use an index.

Think of it as

Every other query operator compares a field to a value; evaluation operators instead run logic. That extra power is also why most of them do not play well with indexes: an index is a sorted structure built for value comparison, and "does this string match a pattern" or "run this JS predicate" usually cannot be answered by walking a sorted structure without checking every candidate — the prefix-anchored regex is the one exception, because "starts with X" is exactly what a sorted index can narrow.

text
db.<collection>.find({ <field>: { $regex: <pattern>, $options: <flags> } })
db.<collection>.find({ <field>: { $mod: [ <divisor>, <remainder> ] } })
db.<collection>.find({ $expr: { <aggregation expression> } })

What we're doing: Show the same field queried two ways — one usable by an index, one not — and why the difference matters at scale.

anchored-vs-unanchored-regex.txttext
db.users.find({ email: { $regex: "^alice" } })
// -> can use an index on email as a range/prefix scan, like a $gte/$lt pair

db.users.find({ email: { $regex: "alice" } })
// -> unanchored: "alice" could be anywhere in the string, so every value must be checked
1
A ^-anchored pattern narrows to a contiguous range of the sorted index, the same way a prefix range query would.
3
Without the anchor, the pattern could match in the middle of any string, so an index cannot rule any value out ahead of time.

Why this works: This is the one regex fact worth memorizing before writing any pattern query against a collection of meaningful size: anchoring at the start is not just a stylistic choice, it is the difference between an index-assisted query and a full collection scan.

Reaching for $where for a check that $expr or a plain operator already covers

Wrong

text
db.orders.find({ $where: "this.shipped > this.ordered" })

Better

text
db.orders.find({ $expr: { $gt: ["$shipped", "$ordered"] } })

What you see: A query that compares two fields on the same document runs far slower than expected, and cannot use any index at all.

Why: $where evaluates a JavaScript string once per document with no query-planner optimization available; $expr lets the same field-to-field comparison run through MongoDB's own expression engine, which can be partially index-optimized where $where never can.

The evaluation operators

The evaluation operators
OperatorPurposeIndex-friendly?
$regexpattern match a string fieldonly if anchored at the start (^prefix)
$moddivisor/remainder check on a numberno
$expraggregation-style expression, incl. field-to-field comparisonsometimes, depends on the expression
$textfull-text search against a text indexyes — requires a text index
$wherearbitrary JavaScript predicate per documentno — slowest, evaluates every document

Together

text
db.users.find({ email: { $regex: "^alice", $options: "i" } })   // can use an index on email
db.users.find({ email: { $regex: "gmail.com$" } })              // cannot — unanchored
db.orders.find({ $expr: { $gt: ["$shipped", "$ordered"] } })     // compares two fields

Remember: $regex/$mod/$expr/$where run logic, not plain comparison. Only a ^-anchored $regex can use an index — $where never can.

See also: compound index interaction · comparison operators

Dot notation for nested fields

corebeginner

Dot notation reaches into an embedded document or a specific array index using a quoted string like "address.city" or "tags.0" — the query still targets one field path, just a nested one.

Think of it as

A query field name is always a string, so "address.city" is not special syntax the parser treats differently — it is a path that MongoDB walks: first into the address subdocument, then to its city field. The same path-walking applies to an array index ("tags.0"), because BSON arrays are internally keyed by position the same way a subdocument is keyed by field name.

text
db.<collection>.find({ "<parent>.<child>": <value> })  // or "<array>.<index>"

What we're doing: Show dot notation reaching into a two-level embedded document, and the quoting requirement that trips up readers coming from plain JS object access.

dot-notation.txttext
db.users.insertOne({
  name: "Alan",
  contact: { phone: { type: "cell", number: "111-222-3333" } },
})

// WRONG in a query filter: contact.phone.number is not valid JS/BSON syntax here
// db.users.find({ contact.phone.number: "111-222-3333" })

db.users.find({ "contact.phone.number": "111-222-3333" })
// -> matches, because the whole path is one quoted string key
6
Unquoted dots are not valid object-key syntax in the query document itself — this is a syntax error, not a logic error.
9
The full path must be one quoted string — MongoDB parses the dots internally to walk the nested structure.

Why this works: It is easy to reach for real nested-object syntax ({ contact: { phone: { number: ... } } }) out of habit, but that means something different in a query filter — it would require phone to be *exactly* that one-field object, not just contain that field among others.

Using nested-object filter syntax instead of a dotted string path

Wrong

text
db.users.find({ contact: { phone: { number: "111-222-3333" } } })
// requires contact.phone to be EXACTLY { number: "111-222-3333" }, no other fields

Better

text
db.users.find({ "contact.phone.number": "111-222-3333" })

What you see: A query meant to match on one nested field returns nothing, because real documents also carry a sibling field like "type" inside the same subdocument.

Why: A nested object in a query filter is matched for exact equality of the whole subdocument, not "contains this field" — dot notation is what expresses "this one field within the subdocument, regardless of its siblings."

Reading a dotted path

"contact.phone.number"

contact

top-level field — an embedded document on the target collection

phone

nested field — an embedded document inside contact

number

leaf field — the value actually being matched

  • Whole: "contact.phone.number"
  • contact — top-level field: an embedded document on the target collection
  • phone — nested field: an embedded document inside contact
  • number — leaf field: the value actually being matched

Dot notation paths

Dot notation paths
PathTargetsExample
"a.b"field b inside embedded document a{ "address.city": "Austin" }
"a.b.c"field c inside a.b, two levels deep{ "contact.phone.number": "555-1234" }
"arr.0"the first (index 0) element of array arr{ "tags.0": "featured" }
"arr.field"field on any element of an array of documents{ "results.subject": "math" }

Together

text
db.users.find({ "address.city": "Austin" })
db.users.find({ "contact.phone.number": "555-1234" })
db.products.find({ "tags.0": "featured" })

Remember: "parent.child" and "array.N" are quoted string paths MongoDB walks — not real object nesting in the filter.

See also: nested fields and dot notation · array operators

How compound query predicates interact with compound indexes

standardadvanced

A compound index on { a: 1, b: 1, c: 1 } efficiently serves queries on a, a+b, or a+b+c, in that order — the prefix rule. Skipping the first field(s) means the index cannot narrow the search well, or at all.

Think of it as

A compound index is one sorted structure, sorted by its first field, then its second within ties on the first, and so on — like a phone book sorted by last name then first name. You can efficiently jump to "Smith" or "Smith, John," but you cannot efficiently jump to "everyone whose first name is John" without scanning the whole book, because first name alone is not how the book is ordered.

text
db.<collection>.createIndex({ <field1>: 1, <field2>: 1, <field3>: 1 })  // order defines the valid prefixes

What we're doing: Show a query that skips the leading index field and cannot use the index as a narrowing structure.

skipped-prefix.txttext
db.inventory.createIndex({ item: 1, location: 1, stock: 1 })

db.inventory.find({ item: "widget", stock: { $gt: 50 } })
// -> uses the index up to the "item" prefix, then checks stock without index help (partial use)

db.inventory.find({ location: "east", stock: { $gt: 50 } })
// -> cannot use this index efficiently at all — location is not a leading prefix
3
Filtering on item (the leading field) plus stock (the third field, skipping location) still gets partial index help from the item prefix.
6
Filtering on location and stock without item gets none of the index's narrowing benefit — MongoDB falls back to scanning broadly.

Why this works: This is why index field order is a design decision, not an afterthought: it should match the field(s) most queries filter on first, with more selective or more commonly-combined fields earlier in the definition.

Creating a compound index in an order that does not match how the app actually queries

Wrong

text
// app almost always queries by location first, but the index was created:
db.inventory.createIndex({ item: 1, location: 1, stock: 1 })

Better

text
db.inventory.createIndex({ location: 1, item: 1, stock: 1 })  // matches the real query pattern

What you see: An index exists on the right fields, yet the most common query in the app still runs a broad, slow scan.

Why: The index is only efficient for queries that match its leading prefix — if the app's real filter pattern leads with a field that is not first in the index definition, the index provides little or no benefit for that query.

Prefix rule — sorted by item, then location, then stock

{ item }

valid prefix — usable alone

{ item, location }

valid prefix — usable together

{ item, location, stock }

the full index

  1. { item } — valid prefix — usable alone
  2. { item, location } — valid prefix — usable together
  3. { item, location, stock } — the full index

Prefix rule for index { item: 1, location: 1, stock: 1 }

Prefix rule for index { item: 1, location: 1, stock: 1 }
Query filters onUses index efficiently?
itemyes — matches the { item } prefix
item, locationyes — matches the { item, location } prefix
item, location, stockyes — matches the full index
locationno — location alone is not a prefix
location, stockno — skips the leading item field
item, stockpartially — uses the { item } prefix, then filters stock without index help

Together

text
// index: db.inventory.createIndex({ item: 1, location: 1, stock: 1 })

db.inventory.find({ item: "widget" })                              // efficient
db.inventory.find({ item: "widget", location: "east" })            // efficient
db.inventory.find({ location: "east" })                            // cannot use this index efficiently

Remember: A compound index only helps queries matching its leading field(s), in the order it was defined — skipping the first field gets little or no benefit.

See also: evaluation operators and regex · comparison operators

Advertisement