Array Update Operators and arrayFilters

Intermediate
12 min

Array Update Operators and arrayFilters

$set and $inc cover scalar fields, but arrays need their own vocabulary: append, de-duplicate, remove by condition, and modify one, some or all elements in place. MongoDB provides all of that as atomic operators, so you never have to read an array, edit it in application code and write it back. After this lesson you will be able to grow and trim arrays safely, remove elements by value or condition, and target specific elements with the positional operators and arrayFilters.

Adding Elements: $push and $addToSet

$push appends a value; $addToSet appends only if the value is not already present. Both create the array if the field does not exist yet.

javascript
db.products.updateOne({ sku: "DESK-1" }, { $push: { tags: "sale" } }) db.products.updateOne({ sku: "DESK-1" }, { $addToSet: { tags: "sale" } }) // no duplicate // Several values at once db.products.updateOne({ sku: "DESK-1" }, { $addToSet: { tags: { $each: ["wood", "office"] } } })

$push accepts modifiers that turn it into a small array-maintenance toolkit:

| Modifier | Effect | |---|---| | $each | push several values | | $position | insert at an index instead of the end (0 = front) | | $slice | keep only the first n (n) or last n (-n) elements after the push | | $sort | sort the array (1, -1, or { field: 1 } for sub-documents) before slicing |

javascript
// "Recent activity" list capped at the 10 newest entries db.users.updateOne( { _id: userId }, { $push: { activity: { $each: [{ type: "login", at: new Date() }], $slice: -10 } } } )

This capped-array pattern is how you keep embedded arrays bounded, which protects you from documents growing toward the 16 MB limit. $addToSet compares sub-documents as whole values, so { a: 1, b: 2 } and { b: 2, a: 1 } count as different.

Removing Elements: $pop, $pull and $pullAll

javascript
db.queue.updateOne({ _id: 1 }, { $pop: { items: -1 } }) // remove first element db.queue.updateOne({ _id: 1 }, { $pop: { items: 1 } }) // remove last element db.products.updateOne({ sku: "DESK-1" }, { $pull: { tags: "sale" } }) // by value db.products.updateOne({ sku: "DESK-1" }, { $pull: { scores: { $lt: 50 } } }) // by condition db.products.updateOne({ sku: "DESK-1" }, { $pull: { variants: { stock: 0 } } }) // sub-documents db.products.updateOne({ sku: "DESK-1" }, { $pullAll: { tags: ["old", "clearance"] } })

$pull removes every element that matches its condition; there is no "remove first match" variant. For sub-documents the condition is a query on the element, so { stock: 0 } and { stock: { $lte: 0 }, color: "white" } both work.

Updating One Element: the Positional $ Operator

$ stands for the index of the first array element matched by the query. The array field must appear in the filter for $ to be resolvable.

javascript
db.products.updateOne( { sku: "DESK-1", "variants.color": "oak" }, { $set: { "variants.$.stock": 10 }, $inc: { "variants.$.sold": 1 } } )

Limitations: it targets one element only, it cannot be combined with upsert to insert elements, and it does not descend into nested arrays. Those cases are covered by the operators below.

Updating Every Element: $[]

javascript
// Increase stock of all variants by 5 db.products.updateOne({ sku: "DESK-1" }, { $inc: { "variants.$[].stock": 5 } })

Updating Matching Elements: $[identifier] and arrayFilters

$[name] acts on every element for which the matching entry in arrayFilters is true. The identifier must start with a lowercase letter and is referenced as name.field inside the filter.

javascript
db.products.updateMany( { "variants.stock": 0 }, { $set: { "variants.$[v].status": "out-of-stock" } }, { arrayFilters: [{ "v.stock": 0 }] } ) // Nested arrays: one identifier per level db.orders.updateOne( { _id: orderId }, { $inc: { "items.$[i].parts.$[p].qty": 1 } }, { arrayFilters: [{ "i.sku": "DESK-1" }, { "p.id": 3 }] } )

Every identifier used in the path must appear in arrayFilters, and unlike $ the array does not need to be part of the query filter.

Common Mistakes

  • Reading, modifying and rewriting the whole array in application code. It loses concurrent updates; the operators above are atomic on the server.
  • Using $push where $addToSet was intended, producing duplicate tags.
  • Expecting $ to update every match. It updates the first one; use $[] or $[id].
  • Letting arrays grow without bound. Cap them with $slice, or move the data to its own collection.
Quick Quiz
Question 1 of 3

Which update removes every score below 50 from `scores: [30, 70, 45, 90]`?

Key Takeaways

  • $push appends, $addToSet appends without duplicates; $each, $position, $sort and $slice refine $push.
  • $pop removes from either end, $pull removes all elements matching a value or condition, $pullAll removes a list of values.
  • $ updates the first element matched by the query; $[] updates all elements.
  • $[id] with arrayFilters updates exactly the elements that satisfy a condition, including inside nested arrays.
  • All array operators run atomically on the server, so you never need a read-modify-write cycle.

Next lesson: Upserts, findOneAndUpdate and Bulk Writes — insert-or-update in one call, read and modify atomically, and batch many writes into a single request.

Array Update Operators and arrayFilters - MongoDB | CodeYourCraft | CodeYourCraft