Array Query Operators
Arrays are everywhere in MongoDB documents, and querying them correctly requires a slightly different mental model than querying scalar fields — MongoDB compares against any element of the array by default, which is powerful but has real gotchas once you need to match multiple conditions together.
Exact Array Match
Comparing a field directly to an array literal requires the array to match exactly — same elements, same order, same length.
Exact match — order and length must match
db.posts.insertOne({ _id: 1, tags: ["mongodb", "database", "nosql"] })
db.posts.find({ tags: ["mongodb", "database", "nosql"] }) // matches
db.posts.find({ tags: ["database", "mongodb", "nosql"] }) // does NOT match — order differs
db.posts.find({ tags: ["mongodb", "database"] }) // does NOT match — length differsElement Match (Implicit $in Semantics)
Comparing a field to a single scalar value matches if the array contains that value anywhere, regardless of position — this is the query pattern you'll use constantly.
Single value matches any array element
db.posts.find({ tags: "mongodb" })
// matches any document whose "tags" array contains "mongodb" anywhere$all — Every Value Must Be Present
$all — array must contain every listed value
db.posts.find({ tags: { $all: ["mongodb", "nosql"] } })
// matches documents whose tags array contains BOTH "mongodb" AND "nosql",
// in any order, alongside any other tags$elemMatch — Why It Matters
The single biggest gotcha in array querying: when an array holds embedded documents and you filter on more than one field of that sub-document, separate conditions can each match a different element, producing false positives. $elemMatch forces all conditions to match the same array element.
Without $elemMatch — the cross-matching bug
db.orders.insertOne({
_id: 1,
items: [
{ sku: "A", qty: 1, price: 100 },
{ sku: "B", qty: 5, price: 10 }
]
})
// Intent: "find an item with qty >= 5 AND price >= 100" — should match NOTHING
db.orders.find({ "items.qty": { $gte: 5 }, "items.price": { $gte: 100 } })
// BUT this matches! qty>=5 is satisfied by item B, price>=100 is satisfied by item A —
// two DIFFERENT elements each satisfy one condition.With $elemMatch — correct single-element match
db.orders.find({
items: { $elemMatch: { qty: { $gte: 5 }, price: { $gte: 100 } } }
})
// Correctly matches only if ONE SAME array element satisfies both conditions.
// The order above has no single item with qty >= 5 AND price >= 100, so it returns nothing.$elemMatch. Filtering with separate dot-notation conditions on an array of objects is a silent correctness bug waiting to happen.$size — Array Length
Matching by array length
db.posts.find({ tags: { $size: 3 } }) // exactly 3 elements
// Note: $size does NOT support ranges like $gt — for "more than 3 tags"
// you need a computed field or an aggregation with $expr + $size.
db.posts.find({ $expr: { $gt: [{ $size: "$tags" }, 3] } }) // more than 3 tagsDot-Notation Index Queries
You can address a specific array index directly with dot notation, though this is less common than element-match or $elemMatch queries since it hard-codes position.
Querying by array index
db.orders.find({ "items.0.sku": "A" }) // first item's sku is "A"Matching Nested-Document Arrays
Combining $elemMatch with nested logic
db.students.insertOne({
_id: 1,
grades: [
{ subject: "Math", score: 95 },
{ subject: "Art", score: 60 }
]
})
// Students with a Math grade of 90+
db.students.find({
grades: { $elemMatch: { subject: "Math", score: { $gte: 90 } } }
})Projection with the Positional $ and $slice
By default, a query returns the whole array even if only one element matched the filter. Use the positional $ projection operator to return just the matching element, or $slice to return a sub-range regardless of match.
Positional $ projection
// Return only the FIRST matching item from "items", not the whole array
db.orders.find(
{ _id: 1, "items.sku": "B" },
{ "items.$": 1 }
)
// { _id: 1, items: [ { sku: "B", qty: 5, price: 10 } ] }$slice — return a sub-range of an array
db.posts.find({}, { comments: { $slice: 5 } }) // first 5 comments
db.posts.find({}, { comments: { $slice: -5 } }) // last 5 comments
db.posts.find({}, { comments: { $slice: [10, 5] } }) // skip 10, then take 5$ projection only returns the first matching array element, mirroring the positional $ update operator's "first match only" behavior. Use the aggregation framework's $filter operator when you need every matching element, not just the first.A bare scalar value against an array field matches if the array contains it anywhere.
$all— array must contain every listed value, any order.$elemMatch— required whenever multiple conditions must hold on the SAME embedded-document array element.$size— exact array length only; use$expr+$sizefor ranges.items.$projection returns just the first matched array element;$slicereturns a sub-range unconditionally.