skip to content

How do the positional $ operator, $[] and arrayFilters differ when updating arrays?

level: middleimportance: must knowfreq 68%

answer

  1. Three placeholders, three scopes
  2. One of them stops after the first hit
  3. Which one needs the array named in the filter
  4. One form takes an extra option object
  5. Only one can update several chosen elements

basics

~20 s

The positional $ updates only the first array element matched by the query filter. $[] updates every element unconditionally. $[identifier] with arrayFilters updates exactly the elements matching the given condition, and is the only form that can update several specific elements.

solid answer

~40 s

All three are placeholders for an array index inside an update path. `"items.$.qty"` uses the **positional operator**: it resolves to the first element that the query matched, so the array field must appear in the filter, and only one element is ever changed. `"items.$[].qty"` is the **all-positional** form and applies to every element of the array, with no condition. `"items.$[el].qty"` is the **filtered positional** form: you supply `arrayFilters: [ { "el.sku": "abc" } ]`, and every element satisfying that condition is updated. Practically: `$` for "the one I already matched", `$[]` for "all of them", `arrayFilters` for "these specific ones", including the classic interview task of updating the *third* matching element — which plain `$` cannot do. arrayFilters also handles nested arrays, e.g. `"a.$[o].b.$[i].x"` with two filter entries.

code

javascript · 12 lines
javascript
// only the FIRST element with sku "abc" is changed
db.orders.updateOne(
  { _id: 1, "items.sku": "abc" },
  { $set: { "items.$.qty": 5 } }
)

// EVERY element with sku "abc" is changed
db.orders.updateOne(
  { _id: 1 },
  { $set: { "items.$[el].qty": 5 } },
  { arrayFilters: [ { "el.sku": "abc" } ] }
)

go deeper

for a junior

Recall that the positional $ changes only the first matched array element and that arrayFilters exists for changing specific ones; be able to read an update path containing $[el].

for a middle

Explain all three placeholders, why $ requires the array in the query filter, the arrayFilters identifier rules, and how to update the third matching element.

for a senior

Show judgment on stable element identifiers versus index paths, handle nested arrays with two arrayFilters entries, and spot filters that need $elemMatch before the positional operator resolves correctly.

for a principal

Own the modelling consequence: when line items need their own stable ids, when array-in-place updates stop being the right shape, and when the elements belong in a separate collection instead.

## The problem these solve When a document holds an array of embedded documents, you often need to change a field inside one specific element without rewriting the array. MongoDB solves this with placeholders in the dotted update path that stand for an array index the server computes at write time. There are three, and they answer three different questions. ## The positional operator: $ ```js db.orders.updateOne( { _id: 1, "items.sku": "abc" }, { $set: { "items.$.qty": 5 } } ) ``` `$` resolves to the index of the **first** array element that satisfied the query condition on that array. Three rules follow from that definition: 1. **The array field must appear in the query filter.** If the filter does not include a condition on `items`, there is no match to refer to and the update fails. 2. **Only the first match is updated.** If three elements have `sku: "abc"`, exactly one changes. This is the single most common surprise in production code. 3. **It cannot be used with an upsert that inserts.** There is no matched array element in a newly created document, so an insert path with `$` errors. You also cannot use `$` more than once in a path, so it cannot reach into a nested array. ## The all-positional operator: $[] ```js db.orders.updateOne( { _id: 1 }, { $inc: { "items.$[].qty": 1 } } ) ``` `$[]` stands for every index of the array. No condition on the array is needed in the filter, and every element is modified. Use it for blanket changes — normalizing a field on all elements, incrementing every counter, stamping every element with a flag. ## The filtered positional operator: $[identifier] with arrayFilters ```js db.orders.updateOne( { _id: 1 }, { $set: { "items.$[el].qty": 5 } }, { arrayFilters: [ { "el.sku": "abc" } ] } ) ``` `$[el]` stands for every index whose element matches the `arrayFilters` entry named `el`. This is the general form and the one to reach for by default: - It updates **all** matching elements, not just the first. - It does not require the array to appear in the query filter, so the filter can stay a plain `_id` lookup. - It works with upserts, because the condition is independent of what the query matched. - It composes for nested arrays: `"a.$[outer].b.$[inner].x"` with `arrayFilters: [ { "outer.k": 1 }, { "inner.k": 2 } ]`. The rules the server enforces: every identifier used in the update must have exactly one matching entry in `arrayFilters`, and every entry in `arrayFilters` must be used by the update — an unused filter or an undeclared identifier is an error. Identifiers must begin with a lowercase letter and contain only alphanumeric characters. Filtered positional updates require MongoDB 3.6 or later, so every currently supported release has them. ## "Update the third matching element" This is the question interviewers like, because it exposes whether you actually know the three forms. Plain `$` gives you the first match only. `$[]` gives you all of them. Neither gives you "the third". Your options are: - If you know the **array index**, address it directly: `{ $set: { "items.2.qty": 5 } }`. That is a plain dotted path, not a positional operator, and it is exact but brittle — the index changes whenever elements are inserted or removed. - If the element has a **stable identifier**, use `arrayFilters` on that identifier: `{ "el.lineId": "L3" }`. This is the durable answer, and it is the reason line items usually carry their own id. - If "third" is genuinely positional and unstable, you generally need to restructure: read the document, find the index, and write with an index path plus a guard in the filter so a concurrent change is detected — or move the elements into their own collection. ## Which one to reach for Use `$` when you have already matched on the array in the filter and you know at most one element can match — it is the shortest form and needs no extra option. Use `$[]` for genuinely blanket changes. Use `arrayFilters` everywhere else: it says exactly which elements you mean, updates all of them, and does not silently stop at the first. A final note on matching semantics: a filter like `{ "items.sku": "abc", "items.qty": 0 }` matches a document where *some* element has that sku and *some* (possibly different) element has that quantity. When your condition genuinely means "one element satisfies both", the filter needs `$elemMatch` — and `$` then resolves against that match. Getting the filter wrong and the positional operator right still updates the wrong element.

  • Why does an update using the positional $ fail when the query filter does not mention the array?
    Because `$` is defined as the index of the element that the **query** matched. With no condition on that array there is no matched element for it to resolve to, so the server rejects the update rather than guessing. The same reasoning is why `$` cannot be used on the insert path of an upsert: a newly created document matched nothing.
  • How do you update an element inside an array nested within another array?
    Use two filtered positional placeholders and two arrayFilters entries, for example path `"orders.$[o].items.$[i].qty"` with `arrayFilters: [ { "o.id": 7 }, { "i.sku": "abc" } ]`. The plain positional `$` cannot do this: it may appear only once in a path, so it can never reach a second level of nesting.
  • What happens if arrayFilters declares an identifier the update path never uses?
    The write is rejected. Every arrayFilters entry must be referenced by the update document, and every identifier in the update must have exactly one arrayFilters entry. That strictness catches typos in the identifier name, which would otherwise silently update nothing or everything.

Think of the three placeholders as find-and-replace scopes in an editor: $ is Replace on the first hit, $[] is Replace All, and $[el] with arrayFilters is Replace All Within Selection.

saying these in an interview costs you the question

  • Thinks the positional $ updates every matching array element
  • Uses $ without any array condition in the filter
  • Believes $[] takes a condition to limit which elements change
  • Assumes arrayFilters entries are optional hints
  • Thinks a hard-coded index path is a positional operator

context