skip to content

Why can $elemMatch return different results from listing the same two conditions on an array field?

level: middleimportance: must knowfreq 72%

answer

  1. Two predicates, two possibly different witnesses
  2. Ask: same element, or any elements?
  3. [95, 20] satisfies both bounds separately
  4. One element must satisfy the whole inner document
  5. Scalar fields never match $elemMatch

basics

~10 s

Two conditions written side by side may be satisfied by different array elements; $elemMatch requires one single element to satisfy all of them at once. On a non-array field the two forms are equivalent.

solid answer

~50 s

When a field holds an array, MongoDB evaluates each predicate independently against the elements. `{ scores: { $gte: 80, $lt: 90 } }` therefore matches `[95, 20]`, because 95 satisfies `$gte: 80` and 20 satisfies `$lt: 90` — no element satisfies both. `{ scores: { $elemMatch: { $gte: 80, $lt: 90 } } }` matches only if at least one element satisfies every condition inside it, so `[95, 20]` is rejected and `[85]` accepted. The same split appears with arrays of subdocuments: `{ "items.price": { $gt: 100 }, "items.qty": { $lt: 5 } }` can be satisfied by two different items, while `{ items: { $elemMatch: { price: { $gt: 100 }, qty: { $lt: 5 } } } }` demands one item that is both expensive and low in stock. Use `$elemMatch` whenever two or more conditions must hold for the *same* element.

code

javascript · 10 lines
javascript
db.c.insertMany([
  { _id: 1, scores: [95, 20] },
  { _id: 2, scores: [85] }
])

// conditions evaluated independently -> _id 1 and 2
db.c.find({ scores: { $gte: 80, $lt: 90 } })

// one element must satisfy both -> _id 2
db.c.find({ scores: { $elemMatch: { $gte: 80, $lt: 90 } } })

go deeper

for a junior

Recall that when a field is an array, two conditions written together can be satisfied by two different elements, and that $elemMatch is the operator that forces one element to satisfy all of them.

for a middle

Walk through a concrete array such as [95, 20] against both filter forms and explain the per-predicate evaluation rule that produces the difference, including the array-of-subdocuments case with dotted paths.

for a senior

Show how this defect survives testing — fixtures with single-element arrays make both forms agree — and how you would audit existing queries, plus what explain() does and does not show about $elemMatch and multikey indexes.

for a principal

Frame it as a modelling and review problem: decide when array-of-subdocuments is the right shape at all, and put a standard in place so every multi-condition array filter states whether the conditions must share an element.

## How array predicates are evaluated MongoDB matches a predicate against an array field by testing the array's own value and then each element, and it does this **per predicate, independently**. A filter that names the same field twice — or names one field with two operators — is a conjunction of separate predicates, each of which may find its own witness element. Nothing in that model ties the witnesses together. That is the whole story behind the classic interview question. Consider: ``` { _id: 1, scores: [95, 20] } { _id: 2, scores: [85] } { _id: 3, scores: [70, 60] } ``` `db.c.find({ scores: { $gte: 80, $lt: 90 } })` returns documents 1 **and** 2. Document 1 qualifies because 95 satisfies `$gte: 80` and 20 satisfies `$lt: 90`. Almost no one writing that filter meant to include it. `db.c.find({ scores: { $elemMatch: { $gte: 80, $lt: 90 } } })` returns document 2 only. `$elemMatch` changes the unit of evaluation: the inner document is applied as a whole to each element in turn, and the document matches if some element satisfies all of it. ## Arrays of subdocuments The same rule governs dotted paths into arrays of embedded documents, and here the consequences are usually worse because the fields look unrelated. ``` { _id: 1, items: [ { sku: "A", price: 200, qty: 50 }, { sku: "B", price: 5, qty: 2 } ] } ``` `{ "items.price": { $gt: 100 }, "items.qty": { $lt: 5 } }` matches this document: item A supplies the expensive half, item B supplies the low-stock half. If the intent was "an expensive item that is nearly out of stock", the query is silently wrong and will look right in a small fixture where each document has one item. `{ items: { $elemMatch: { price: { $gt: 100 }, qty: { $lt: 5 } } } }` expresses the intent: one element must satisfy both. ## When the two forms agree On a non-array field the forms are equivalent — there is only one value to test, so independent evaluation and per-element evaluation coincide. `{ price: { $elemMatch: { $gt: 100 } } }` on a scalar `price` matches nothing at all, because `$elemMatch` only ever matches array elements. That is a real trap when a field's shape varies across documents. With a single condition, `$elemMatch` is redundant for scalars in arrays: `{ tags: { $elemMatch: { $eq: "red" } } }` and `{ tags: "red" }` select the same documents. It is not redundant for subdocuments where the single condition is itself compound, and it is not redundant when you want to be explicit that the field is expected to be an array. ## Syntax rules worth remembering The argument to `$elemMatch` is a document of conditions. For arrays of subdocuments those conditions are field names: `{ $elemMatch: { price: { $gt: 100 } } }`. For arrays of scalars they are operator expressions applied to the element itself: `{ $elemMatch: { $gte: 80, $lt: 90 } }`. Mixing the two — writing a bare value where an operator belongs, or a field name for a scalar array — produces a query that quietly matches nothing. `$elemMatch` may be nested to reach into arrays inside array elements, and it may be combined with `$all`: `{ items: { $all: [ { $elemMatch: { sku: "A" } }, { $elemMatch: { sku: "B" } } ] } }` requires two distinct-condition witnesses in the same array. ## Index behaviour An index on an array field is multikey: one entry per element. A single condition inside `$elemMatch` can be served from that index. When `$elemMatch` carries several conditions on different fields of a subdocument, the index can supply candidate elements for one bound and the server verifies the rest against the fetched document, because separate index entries cannot prove that two conditions hit the same element. Expect `$elemMatch` to narrow correctness, not necessarily to narrow the number of documents examined. ## How to decide Ask one question of every multi-condition array filter: *must these conditions hold for the same element?* If yes, `$elemMatch`. If genuinely no — "a customer who has at least one order over £100 and at least one order marked returned", where different orders are fine — the side-by-side form is correct and `$elemMatch` would be wrong. Both intents are legitimate; the bug is not choosing deliberately.

  • Is { tags: { $elemMatch: { $eq: "red" } } } different from { tags: "red" }?
    For an array of scalars they select the same documents — a single condition has nothing to be tied to, so $elemMatch adds no constraint. The difference is at the edges: $elemMatch matches only array-valued fields, so a document where tags is the scalar string "red" matches the plain equality form but not the $elemMatch form. Use the plain form unless you deliberately want to exclude scalars.
  • When is the side-by-side form the correct query rather than the bug?
    Whenever the conditions are genuinely independent facts about the array as a whole. "A customer with at least one order over 100 and at least one returned order" is satisfied by two different orders, and $elemMatch would wrongly demand a single order that is both. The form is only a bug when the intent was a single element; the real failure is not making the choice consciously.
  • Does $elemMatch let the query use an index on the array field?
    Partly. An index on an array is multikey, storing one entry per element, so one bound inside the $elemMatch can be answered from the index. Separate index entries cannot prove that two conditions matched the same element, so the server fetches candidate documents and verifies the remaining conditions. Treat $elemMatch as a correctness tool first; check explain output before assuming it reduced the documents examined.

saying these in an interview costs you the question

  • Says the two forms are always equivalent
  • Thinks $elemMatch is only about projecting array elements
  • Uses $elemMatch on a scalar field and expects a match
  • Writes field names inside $elemMatch for an array of scalars
  • Assumes two dotted-path conditions must hit the same subdocument

context