Querying Arrays and Embedded Documents

Intermediate
12 min

Querying Arrays and Embedded Documents

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.

Embedded Documents and Dot Notation

To filter on a field inside an embedded document, quote the path with a dot:

javascript
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 depth

Querying 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.

Matching Values in Arrays

When a field holds an array, a plain equality condition matches if any element equals the value:

javascript
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.

The Multi-Element Trap and $elemMatch

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:

javascript
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.

Arrays of Embedded Documents

The same rule applies to arrays of sub-documents, where it bites most often:

javascript
// 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.

Returning Only the Matching Element

Projection can trim arrays to the elements that matter:

javascript
// $ 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 } } } } |

Common Mistakes

  • Using a whole sub-document as a filter when you only care about one field; field order and extra fields make it fail.
  • Combining range conditions on an array without $elemMatch, matching across different elements.
  • Forgetting that $ne and $nin also match documents where the array is absent. Add $exists: true when that matters.
Quick Quiz
Question 1 of 3

A document has `scores: [40, 100]`. Which query does NOT match it?

Key Takeaways

  • Use dot notation ("dims.w") for nested fields; a whole sub-document in a filter is an exact, order-sensitive match.
  • An equality on an array field matches if any element equals the value; "tags.0" targets a position.
  • $all checks containment, $size checks exact length, $in checks membership.
  • Multiple conditions on one array can be satisfied by different elements; $elemMatch forces a single element to satisfy all of them.
  • The $ 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.

Querying Arrays and Embedded Documents - MongoDB | CodeYourCraft | CodeYourCraft