skip to content

In MongoDB, what does the find filter { tags: "red" } match when tags holds an array?

level: juniorimportance: must knowfreq 78%

answer

  1. Equality looks in two places at once
  2. Arrays are matched element by element
  3. Descent stops after one level
  4. Array literal on the right means exact order
  5. $all for any order, $size for length

basics

~20 s

It matches documents where tags is exactly the string "red" and documents where tags is an array containing "red" as an element. MongoDB applies the equality predicate to the field's value and to each array element.

solid answer

~50 s

`{ tags: "red" }` is shorthand for `{ tags: { $eq: "red" } }`. When MongoDB evaluates an equality predicate it tests the field's own value **and**, if that value is an array, each element of the array one level deep. So it matches `{ tags: "red" }`, `{ tags: ["red", "blue"] }` and `{ tags: ["blue", "red"] }` alike — the query text gives no hint whether the stored field is a scalar or an array. If you instead write `{ tags: ["red", "blue"] }`, that is whole-array equality: it matches only a tags array with exactly those elements in that order (or an array that contains that exact array as an element). To require several values in any order use `$all`; to match on length use `$size`, which takes an exact integer only.

code

javascript · 14 lines
javascript
db.items.insertMany([
  { _id: 1, tags: "red" },
  { _id: 2, tags: ["red", "blue"] },
  { _id: 3, tags: ["blue", "red"] }
])

// element or scalar equality -> _id 1, 2, 3
db.items.find({ tags: "red" })

// whole-array equality, order matters -> _id 2 only
db.items.find({ tags: ["red", "blue"] })

// set containment, any order -> _id 2, 3
db.items.find({ tags: { $all: ["red", "blue"] } })

go deeper

for a junior

Be ready to state that a plain equality filter matches both a scalar field and any element of an array field, and to name $all for order-independent containment and $size for an exact length.

for a middle

Explain the mechanics: implicit $eq, element-level matching that descends exactly one level, and why an array literal on the right-hand side becomes order-sensitive whole-array equality instead of containment.

for a senior

Show where this bites in production — schema drift between scalar and array shapes that reads never reveal, order-sensitive equality passing in tests, and why $size cannot be ranged so a maintained count field is the durable answer.

for a principal

Own the modelling consequence: decide whether multi-valued fields are always arrays (even of length one), how that invariant is enforced at write time, and what it costs downstream when a field's cardinality changes after launch.

## A filter is a document Every `find()` call takes a filter document. Each key names a field (possibly a dotted path) and each value is either a literal or an operator expression. A literal is implicit equality: `{ tags: "red" }` and `{ tags: { $eq: "red" } }` are the same query. The explicit `$eq` form exists so you can combine equality with other operators on the same field, for example `{ tags: { $eq: "red", $ne: "blue" } }`. ## Equality against an array field MongoDB has no separate "contains" operator for the common case, because equality already covers it. When the engine evaluates an equality predicate on a field whose stored value is an array, it succeeds if **either** the whole array equals the operand **or** any single element equals it. That descent goes one level only. Given these documents: ``` { _id: 1, tags: "red" } { _id: 2, tags: ["red", "blue"] } { _id: 3, tags: ["blue", "green"] } { _id: 4, tags: [["red"], "blue"] } ``` `db.items.find({ tags: "red" })` returns documents 1 and 2. Document 3 has no matching element. Document 4 does not match either: its first element is the array `["red"]`, not the string `"red"`, and the engine does not recurse into nested arrays for this predicate. The practical consequence is that the same filter serves a single-valued field and a multi-valued one. That is convenient, and it is also why a field that silently changed from a scalar to an array in some documents can go unnoticed for a long time — reads keep working. ## Whole-array equality Put an array on the right-hand side and the semantics flip toward exactness. `{ tags: ["red", "blue"] }` matches a document whose `tags` is an array with exactly two elements, `"red"` then `"blue"`. `["blue", "red"]` does not match, and neither does `["red", "blue", "green"]`. Because element-level matching still applies, it *also* matches a document whose `tags` array contains `["red", "blue"]` as one of its elements — the nested-array case that surprises people. Order sensitivity is the important half: whole-array equality is almost never what an application wants when the array models a set of labels. ## Order-independent containment: $all `{ tags: { $all: ["red", "blue"] } }` matches any document whose `tags` array contains both values, in any order, with any number of other elements. It is equivalent to `{ $and: [ { tags: "red" }, { tags: "blue" } ] }`. Contrast `$in`, which is a logical OR over the listed values: `{ tags: { $in: ["red", "blue"] } }` matches a document whose array contains *either* value. The mnemonic is `$in` = any of, `$all` = all of, bare array = exactly this array in this order. ## Length: $size `{ tags: { $size: 3 } }` matches documents whose `tags` array has exactly three elements. `$size` takes a literal non-negative integer; you cannot write `{ $size: { $gt: 3 } }`, and there is no range form. Two standard workarounds exist. To test "at least N elements", use the fact that array positions are addressable by dotted path: `{ "tags.2": { $exists: true } }` is true only when a third element exists. For anything richer — sorting by length, filtering by a computed length — maintain a separate counter field that your writes keep in step, which also makes the predicate indexable. ## Where this bites The most common bug is assuming the filter tells you the shape of the data. `{ tags: "red" }` is happy either way, so code that later does `doc.tags.toUpperCase()` breaks on the array documents. Validate the shape at write time rather than inferring it from a successful read. The second is reaching for whole-array equality when you meant containment, then discovering that the query works in tests (where the fixture happens to be written in the same order) and fails in production. The third is putting two conditions on one array field and expecting them to hold for the same element. `{ scores: { $gte: 80, $lt: 90 } }` matches an array like `[95, 20]` because one element satisfies each condition independently; requiring one element to satisfy both is what `$elemMatch` is for. ## Indexing note An index on an array field is a multikey index: it stores one index entry per element, which is exactly why element-level equality can be served from the index. That mechanism is what makes `{ tags: "red" }` cheap on a large collection.

  • How do you match documents whose tags array has more than three elements?
    Not with `$size` — it accepts only an exact integer, never a comparison expression. Use the addressable-position trick: `{ "tags.3": { $exists: true } }` is true only when a fourth element exists, which means length is at least four. For anything more flexible, maintain a `tagCount` field alongside the array and query that; it is also the only version of the predicate an ordinary index can serve efficiently.
  • What is the difference between $in and $all on an array field?
    `$in` is a disjunction over the listed values: `{ tags: { $in: ["red", "blue"] } }` matches a document whose tags array contains at least one of them. `$all` is a conjunction: `{ tags: { $all: ["red", "blue"] } }` requires the array to contain every listed value, in any order and with extra elements allowed. `$in` also accepts regular-expression objects among its values; `$all` is about set containment.
  • Does { tags: "red" } match a document whose tags is [["red"], "blue"]?
    No. Equality descends one level into the array and compares each element to the operand. The first element there is the array `["red"]`, which is not equal to the string `"red"`. To match that document you would query the nested value positionally, `{ "tags.0.0": "red" }`, or restructure the data — arrays of arrays are usually a modelling mistake in MongoDB precisely because query and index semantics stop at one level.

saying these in an interview costs you the question

  • Claims equality on an array field never matches elements
  • Thinks { tags: ["red","blue"] } means contains both values
  • Believes array element order is irrelevant to whole-array equality
  • Writes $size with $gt to filter on array length
  • Assumes a matching filter proves the field is a scalar

context