Update Documents
Data never stays still. Prices change, stock goes down with every sale, orders move from pending to shipped, and customers add new tags to their wish lists. MongoDB changes documents in place with updateOne() and updateMany(), using update operators that say exactly which fields to change and how.
In this lesson you will change single fields, numbers and arrays, update one element inside an array, replace whole documents and read a document back in the same step as updating it. Start with freshly loaded data and use shop; the examples run in order and build on each other.
updateOne() and $set
updateOne(filter, update) finds the first document that matches the filter and applies the update. $set gives a field a new value. The Wireless Mouse goes on sale at ₹749:
db.products.updateOne({ _id: 101 }, { $set: { price: 749 } }){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}matchedCount says how many documents matched the filter and modifiedCount how many actually changed. Run the same command again and you get matchedCount: 1 but modifiedCount: 0, because the price is already 749. (insertedId and upsertedCount belong to upserts, covered in Upserts & Bulk Writes.)
The old orders that contain the mouse still show ₹799. They are separate documents with their own copy of the price, which is exactly what an order history needs.
$set also creates fields that do not exist yet. Give the SSD a warranty and a return policy:
db.products.updateOne({ _id: 107 }, { $set: { warrantyMonths: 12, returnable: true } })
db.products.findOne({ _id: 107 }){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
_id: 107,
name: 'Portable SSD 1TB',
category: 'Storage',
price: 6999,
stock: 25,
rating: 4.7,
tags: [ 'storage', 'backup' ],
specs: { capacity: '1TB', port: 'USB-C' },
returnable: true,
warrantyMonths: 12
}New fields are added at the end of the document. When one update adds several new fields, MongoDB adds them in alphabetical order of their names, which is why returnable comes before warrantyMonths. Use dot notation to set a field inside an embedded document, for example { $set: { 'specs.colour': 'black' } }.
Numbers and fields: $inc, $mul, $unset, $rename
These operators cover most other field changes:
| Operator | What it does | Example |
|---|---|---|
$inc | Adds a number (negative to subtract) | { $inc: { stock: -2 } } |
$mul | Multiplies by a number | { $mul: { price: 2 } } |
$min / $max | Sets the value only if the new one is lower / higher | { $min: { price: 699 } } |
$unset | Removes a field | { $unset: { specs: '' } } |
$rename | Renames a field | { $rename: { qty: 'quantity' } } |
$currentDate | Sets a field to the current date | { $currentDate: { updatedAt: true } } |
$inc is important because it is atomic: the server reads and changes the value in one step, so two shoppers buying at the same moment can never overwrite each other's change. Never read the stock into your program, subtract and $set it back; use $inc.
updateMany(): changing many documents
updateMany() applies the update to every matching document. A delivery van restocks all accessories with 10 more units each:
db.products.updateMany({ category: 'Accessories' }, { $inc: { stock: 10 } }){
acknowledged: true,
insertedId: null,
matchedCount: 5,
modifiedCount: 5,
upsertedCount: 0
}An empty filter, {}, matches every document in the collection, so updateMany({}, ...) changes them all. Run countDocuments() with your filter first if you are not sure how many documents it will touch.
Updating arrays
Arrays have their own operators. $push appends a value, $addToSet appends it only if it is not already there, and $pull removes every element equal to a value (or matching a condition):
db.products.updateOne({ _id: 105 }, { $push: { tags: 'bestseller' } })
db.products.updateOne({ _id: 105 }, { $addToSet: { tags: 'mobile' } })
db.products.updateOne({ _id: 105 }, { $pull: { tags: 'music' } })
db.products.findOne({ _id: 105 }, { tags: 1 }){ _id: 105, tags: [ 'wireless', 'mobile', 'bestseller' ] }(Only the last output is shown.) The $addToSet call reports modifiedCount: 0, because 'mobile' was already a tag. To add several values at once, combine them with $each: { $push: { tags: { $each: ['sale', 'diwali'] } } }. $pop removes the first (-1) or last (1) element.
Updating one element inside an array
Farhan calls to say he wants two mice in pending order 1010, not one. The positional operator $ stands for "the array element that the filter matched":
db.orders.updateOne(
{ _id: 1010, 'items.productId': 101 },
{ $set: { 'items.$.qty': 2 }, $inc: { total: 799 } }
)
db.orders.findOne({ _id: 1010 }, { items: 1, total: 1 }){
_id: 1010,
items: [
{ productId: 106, name: 'Laptop Stand', price: 1299, qty: 1 },
{ productId: 101, name: 'Wireless Mouse', price: 799, qty: 2 }
],
total: 2897
}The filter must mention the array ('items.productId': 101) so MongoDB knows which element $ means. One update can use several operators at once, so the quantity and the total change together, atomically.
The positional $ updates only the first matching element. To update every element that meets a condition, name a placeholder in square brackets and define it with arrayFilters. This renames the cable in every order that contains it:
db.orders.updateMany(
{ 'items.productId': 104 },
{ $set: { 'items.$[line].name': 'USB-C Cable (1 m)' } },
{ arrayFilters: [{ 'line.productId': 104 }] }
){
acknowledged: true,
insertedId: null,
matchedCount: 2,
modifiedCount: 2,
upsertedCount: 0
}$[] with no name means "every element of the array".
Replace, and update-and-return
replaceOne(filter, newDocument) swaps the whole document for a new one, keeping only _id. It takes a plain document with no operators, and any field you leave out is gone, so use it only when you really mean "replace everything".
findOneAndUpdate() updates one document and returns it in the same atomic step. This is the safe way to sell the last units of stock: the filter only matches if stock is available, and you get the new value back:
db.products.findOneAndUpdate(
{ _id: 109, stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after', projection: { name: 1, stock: 1 } }
){ _id: 109, name: 'HD Webcam', stock: 27 }The webcam had 18 units, plus 10 from the restock, minus the one just sold. Without returnDocument: 'after' you get the document as it was before the update. If the stock were 0, nothing would match and the method would return null, so you know the sale failed.
Advanced: updates with a pipeline
Since MongoDB 4.2 the update can be an array of aggregation stages, which lets a field be calculated from other fields of the same document. This stores each product's stock value:
db.products.updateMany({}, [{ $set: { stockValue: { $multiply: ['$price', '$stock'] } } }])Inside the pipeline, '$price' means the current value of price, just as in $expr. With a normal { $set: ... } update, '$price' would be stored as the literal text $price.
When you have finished, reload the shop data with load('shop-data.js') so the next lesson starts clean.
Common mistakes
- Forgetting
$set.updateOne({ _id: 101 }, { price: 749 })is rejected. Wrap changes in an operator. - Using
updateOne()when you meant many. It changes only the first match, even if ten documents match. - Read, change, write in application code. Two requests can overwrite each other. Use
$inc,$pushand conditional filters, which are atomic. - Positional
$without the array in the filter. MongoDB cannot tell which element you mean and returns an error.
Key points
updateOne()changes the first matching document andupdateMany()changes all matches; checkmatchedCountandmodifiedCount.$setchanges or adds fields,$unsetremoves them and$incchanges numbers atomically.$push,$addToSetand$pullchange arrays;$eachadds several values.items.$updates the first matched element;$[name]witharrayFiltersupdates every element that matches.replaceOne()swaps the whole document;findOneAndUpdate()updates and returns it in one step.- Updates can be aggregation pipelines when a new value depends on other fields.
Exercise
Starting from fresh data: (1) mark both shipped orders as delivered and add a deliveredAt field with the current date; (2) add the tag bestseller to the Learn Python Book without creating a duplicate if it is run twice; (3) in order 1012, change the quantity of the Power Bank (product 110) to 2 and update the order total to match.
Show solution
db.orders.updateMany({ status: 'shipped' }, { $set: { status: 'delivered' }, $currentDate: { deliveredAt: true } })
db.products.updateOne({ _id: 108 }, { $addToSet: { tags: 'bestseller' } })
db.orders.updateOne({ _id: 1012, 'items.productId': 110 }, { $set: { 'items.$.qty': 2 }, $inc: { total: 1499 } })The first update matches orders 1007 and 1009. $addToSet makes the second command safe to repeat. In the third, one extra Power Bank adds ₹1,499, so the total becomes ₹3,647, and both changes happen atomically in one update.
Quick Quiz
1. An updateOne() reports matchedCount: 1, modifiedCount: 0. What happened?
A matched document whose fields already had the new values is not modified.
2. Which update adds 'sale' to tags only if it is not already present?
$addToSet skips values already in the array; $set would replace the whole array with a string.
3. Why is { $inc: { stock: -1 } } safer than reading the stock and saving stock - 1?
$inc happens in one step on the server; read-modify-write in application code can lose updates. A filter such as stock: { $gte: 1 } prevents negative stock.
4. What does 'items.$[line].qty' with arrayFilters: [{ 'line.productId': 104 }] update?
The named placeholder $[line] applies the update to every element that matches the array filter.
Learn MongoDB with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
