Updating arrays: $push, $pull, positional operators
Add, remove and modify array elements in place, including the positional $, $[] and filtered $[id] operators.
- Use $push with $each, $slice, $sort
- Use $addToSet, $pull, $pop
- Update matched elements with $, all elements with $[], selected elements with $[id] and arrayFilters
conceptThe concept
| Operator | Effect |
|---|---|
$push | append (with $each, $position, $slice, $sort modifiers) |
$addToSet | append only if not already present |
$pull | remove all elements matching a value or condition |
$pullAll / $pop | remove listed values / first (-1) or last (1) element |
items.$.qty | the first element matched by the query filter |
items.$[].qty | every element |
items.$[it].qty + arrayFilters: [{ "it.category": "books" }] | every element matching the filter |
$push with $each + $slice: -N keeps only the last N elements: a capped array, the safe way to store "recent" items.
whyWhy it matters
Changing one element in place is atomic and sends only the change over the wire. The alternative, reading the array, editing it in the application and writing it back, loses concurrent updates.
Use caseReal-world scenario
A "recently viewed products" list on the customer: $push: { recentlyViewed: { $each: [productId], $slice: -10 } }. Marking every book line in an order as shipped: $set: { "items.$[b].shipped": true } with arrayFilters: [{ "b.category": "books" }].
Try itExamples
Run and edit these in the interactive lesson.
db.customers.updateOne({ _id: 7 }, { $push: { recentlyViewed: { $each: ["P010", "P011", "P012"], $slice: -2 } } })
db.customers.updateOne({ _id: 7 }, { $addToSet: { tags: { $each: ["loyalty", "beta"] } } })
db.customers.findOne({ _id: 7 }, { recentlyViewed: 1, tags: 1 })db.lab_cart.drop()
db.lab_cart.insertOne({ _id: 1, items: [{ sku: "A", qty: 0 }, { sku: "B", qty: 2 }, { sku: "C", qty: 0 }], history: [1, 2, 3] })
db.lab_cart.updateOne({ _id: 1 }, { $pull: { items: { qty: 0 } }, $pop: { history: -1 } })
db.lab_cart.findOne()db.lab_cart.updateOne({ _id: 1, "items.sku": "B" }, { $inc: { "items.$.qty": 5 } })
db.lab_cart.findOne()const o = db.orders.findOne({ "items.2": { $exists: true } })
db.orders.updateOne({ _id: o._id }, { $set: { "items.$[].checked": true } })
db.orders.updateOne({ _id: o._id }, { $mul: { "items.$[big].price": 0.9 } }, { arrayFilters: [{ "big.price": { $gte: 100 } }] })
db.orders.findOne({ _id: o._id }, { items: 1 })internalsUnder the hood
Arrays are BSON documents with numeric keys, so updates rewrite the document in the storage engine (WiredTiger writes a new version; there is no in-place byte patching). Large arrays make every update more expensive and every multikey index bigger. The oplog records array updates as diffs or as the full new array, so a big array updated often also inflates the oplog.
Avoid / preferDo this, not that
db.carts.updateOne({ _id: 1 }, { $set: { items: itemsFromClient } })db.carts.updateOne(
{ _id: 1, "items.sku": "A1" },
{ $inc: { "items.$.qty": 1 } })Rewriting the whole array means the last writer wins and concurrent changes vanish. A positional update changes one element atomically.
db.orders.updateMany({}, { $set: { "items.$.taxed": true } })db.orders.updateMany({}, { $set: { "items.$[].taxed": true } })The positional $ needs the array in the filter and updates only the first match, so this fails. $[] updates every element; $[name] with arrayFilters updates the ones that match a condition.
PitfallsCommon mistakes
- Using
items.$without a filter condition onitems: the server cannot know which element you mean. - Expecting
$to update all matching elements; it updates only the first. Use$[id]with arrayFilters. - Letting arrays grow forever with
$push; add$sliceor move the data to its own collection.
LimitsWhen not to use it
Do not use one document's array as a busy queue or a shared counter list that many clients push to at once: all those writes contend for one document and retry on write conflicts. Give each item its own document instead.
SecuritySecurity notes
A client that controls $push input can grow a document toward the 16 MB limit. Cap arrays with $slice and enforce maxItems in a $jsonSchema validator.
ChallengeTry it yourself
In a scratch collection order_lab, insert one order with three lines, then raise qty by 1 on every fashion line only.
Show one solution
db.order_lab.drop()
db.order_lab.insertOne({ _id: 1, items: [
{ sku: "A", category: "fashion", qty: 1 },
{ sku: "B", category: "books", qty: 2 },
{ sku: "C", category: "fashion", qty: 3 }
] })
db.order_lab.updateOne({ _id: 1 }, { $inc: { "items.$[f].qty": 1 } }, { arrayFilters: [{ "f.category": "fashion" }] })
db.order_lab.findOne()InterviewQuestions you should be able to answer
How do you update all array elements that match a condition?
Use the filtered positional operator: updateMany(filter, { $set: { "items.$[x].status": "shipped" } }, { arrayFilters: [{ "x.category": "books" }] }). $ only updates the first match and $[] updates every element.
How would you store "last 50 activities" per user?
$push with $each and $slice: -50 keeps a bounded array in the user document. Full history goes to a separate collection (or a time series collection) with a TTL or archival policy.
QuizCheck your understanding
4 questions with instant feedback in the interactive lesson; a wrong answer goes into your review deck.