Real documents are rarely flat: an order embeds an address, a product carries a list of tags, a user holds an array of login sessions. MongoDB queries reach into that structure directly, but arrays follow rules that surprise people coming from SQL. After this lesson you will be able to filter on nested fields with dot notation, match array contents with $all, $size and $elemMatch, and avoid the classic "conditions matched different elements" bug.
To filter on a field inside an embedded document, quote the path with a dot:
db.products.find({ "dims.w": 120 })
db.products.find({ "dims.w": { $gte: 100 }, "dims.d": { $lte: 60 } })
db.orders.find({ "shipping.address.city": "Pune" }) // any depthQuerying with a whole embedded document is different: { dims: { w: 120, d: 60 } } is an exact match. The stored sub-document must have exactly those fields, with those values, in that order. { dims: { d: 60, w: 120 } } returns nothing, and so does { dims: { w: 120 } } if d exists. Use dot notation unless you really want byte-for-byte equality.
When a field holds an array, a plain equality condition matches if any element equals the value:
db.products.find({ tags: "office" }) // "office" is one of the tags
db.products.find({ tags: ["office", "wood"] }) // exact array: same elements, same order
db.products.find({ "tags.0": "office" }) // element at index 0
db.products.find({ tags: { $in: ["wood", "metal"] } }) // any element in the list
db.products.find({ tags: { $all: ["office", "wood"] } }) // contains both, any order
db.products.find({ tags: { $size: 2 } }) // exactly two elements$size accepts only an exact number; for "two or more" use { $expr: { $gte: [{ $size: "$tags" }, 2] } }.
Negative conditions also work element-wise: { tags: { $ne: "wood" } } matches documents where no element equals "wood", including documents where tags is missing.
Consider { scores: [70, 95] } and the query { scores: { $gt: 80, $lt: 90 } }. The document matches, because 95 satisfies $gt: 80 and 70 satisfies $lt: 90 — MongoDB does not require the same element to satisfy every condition. $elemMatch does:
db.students.find({ scores: { $gt: 80, $lt: 90 } }) // 70 and 95 → match
db.students.find({ scores: { $elemMatch: { $gt: 80, $lt: 90 } } }) // needs one value in (80, 90)The rule: if a query places two or more conditions on the same array, decide whether they must hold for a single element. If yes, wrap them in $elemMatch.
The same rule applies to arrays of sub-documents, where it bites most often:
// Any variant is white AND any variant has stock — possibly different variants
db.products.find({ "variants.color": "white", "variants.stock": { $gt: 0 } })
// One variant that is white and in stock
db.products.find({ variants: { $elemMatch: { color: "white", stock: { $gt: 0 } } } })
// Exact sub-document match (field order matters)
db.products.find({ variants: { color: "oak", stock: 4 } })The first query returns the Desk because the white variant exists and the oak variant has stock; the second correctly excludes it.
Projection can trim arrays to the elements that matter:
// $ projects the first element matched by the query condition on that array
db.products.find({ "variants.color": "oak" }, { name: 1, "variants.$": 1 })
// $elemMatch in the projection selects independently of the query
db.products.find({}, { name: 1, variants: { $elemMatch: { stock: { $gt: 0 } } } })Both return at most one element; $filter in an aggregation pipeline returns all matches. The array operators covered so far, side by side:
| Operator | Meaning | Example |
|---|---|---|
| field: value | any element equals value | { tags: "office" } |
| $in | any element in list | { tags: { $in: ["a", "b"] } } |
| $all | every listed value present | { tags: { $all: ["a", "b"] } } |
| $size | exact element count | { tags: { $size: 3 } } |
| $elemMatch | one element satisfies all conditions | { v: { $elemMatch: { a: 1, b: { $gt: 2 } } } } |
$elemMatch, matching across different elements.$ne and $nin also match documents where the array is absent. Add $exists: true when that matters.A document has `scores: [40, 100]`. Which query does NOT match it?
"dims.w") for nested fields; a whole sub-document in a filter is an exact, order-sensitive match."tags.0" targets a position.$all checks containment, $size checks exact length, $in checks membership.$elemMatch forces a single element to satisfy all of them.$ and $elemMatch projections return only the first matching array element.Next lesson: Updating Documents — change existing data with updateOne(), updateMany() and the core field operators $set, $unset and $inc.