Update Documents

MongoDB · Lesson 10 of 28 8 min read Updated

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:

MongoDB Shell
db.products.updateOne({ _id: 101 }, { $set: { price: 749 } })
Output
{
  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:

MongoDB Shell
db.products.updateOne({ _id: 107 }, { $set: { warrantyMonths: 12, returnable: true } })
db.products.findOne({ _id: 107 })
Output
{
  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:

OperatorWhat it doesExample
$incAdds a number (negative to subtract){ $inc: { stock: -2 } }
$mulMultiplies by a number{ $mul: { price: 2 } }
$min / $maxSets the value only if the new one is lower / higher{ $min: { price: 699 } }
$unsetRemoves a field{ $unset: { specs: '' } }
$renameRenames a field{ $rename: { qty: 'quantity' } }
$currentDateSets 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:

MongoDB Shell
db.products.updateMany({ category: 'Accessories' }, { $inc: { stock: 10 } })
Output
{
  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):

MongoDB Shell
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 })
Output
{ _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":

MongoDB Shell
db.orders.updateOne(
  { _id: 1010, 'items.productId': 101 },
  { $set: { 'items.$.qty': 2 }, $inc: { total: 799 } }
)
db.orders.findOne({ _id: 1010 }, { items: 1, total: 1 })
Output
{
  _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:

MongoDB Shell
db.orders.updateMany(
  { 'items.productId': 104 },
  { $set: { 'items.$[line].name': 'USB-C Cable (1 m)' } },
  { arrayFilters: [{ 'line.productId': 104 }] }
)
Output
{
  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:

MongoDB Shell
db.products.findOneAndUpdate(
  { _id: 109, stock: { $gte: 1 } },
  { $inc: { stock: -1 } },
  { returnDocument: 'after', projection: { name: 1, stock: 1 } }
)
Output
{ _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:

MongoDB Shell
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, $push and 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 and updateMany() changes all matches; check matchedCount and modifiedCount.
  • $set changes or adds fields, $unset removes them and $inc changes numbers atomically.
  • $push, $addToSet and $pull change arrays; $each adds several values.
  • items.$ updates the first matched element; $[name] with arrayFilters updates 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
MongoDB Shell
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?

2. Which update adds 'sale' to tags only if it is not already present?

3. Why is { $inc: { stock: -1 } } safer than reading the stock and saving stock - 1?

4. What does 'items.$[line].qty' with arrayFilters: [{ 'line.productId': 104 }] update?

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