MongoDBQuerying Arrays In-Depth

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

JS
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 differs
Element 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

JS
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

JS
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

JS
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

JS
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.
Warning
Whenever you filter on two or more fields of the same embedded-document array, reach for $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

JS
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 tags
Dot-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

JS
db.orders.find({ "items.0.sku": "A" })   // first item's sku is "A"
Matching Nested-Document Arrays

Combining $elemMatch with nested logic

JS
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

JS
// 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

JS
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
Note
The positional $ 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 + $size for ranges.

  • items.$ projection returns just the first matched array element; $slice returns a sub-range unconditionally.