$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.
$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.
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 |
// "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.
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.
$ 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.
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.
// Increase stock of all variants by 5
db.products.updateOne({ sku: "DESK-1" }, { $inc: { "variants.$[].stock": 5 } })$[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.
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.
$push where $addToSet was intended, producing duplicate tags.$ to update every match. It updates the first one; use $[] or $[id].$slice, or move the data to its own collection.Which update removes every score below 50 from `scores: [30, 70, 45, 90]`?
$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.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.