Data changes constantly: prices go up, stock goes down, orders move from "paid" to "shipped", users update their profiles. In MongoDB, you change documents with updateOne and updateMany. Each takes a filter, which chooses the documents, and an update document describing what to change.
The update document uses update operators. $set sets fields to new values, $inc adds to a number, $unset removes a field, and there are more for renaming fields, keeping minimums and maximums, recording timestamps and, as you'll see in the arrays lesson, changing array elements. Operators change only the fields you mention and leave the rest of the document alone.
Updates to a single document are atomic: if two requests run $inc on the same stock count at the same moment, both changes are applied correctly. In this lesson you'll use the main operators, read update results, create documents automatically with upsert, replace documents entirely, and fetch the updated document in one step with findOneAndUpdate.
updateOne and updateMany
db.products.updateOne({ _id: 2 }, { $set: { price: 26 } })
// { acknowledged: true, insertedId: null, matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }
db.products.updateMany({ category: 'kitchen' }, { $set: { onSale: true } })updateOnechanges the first matching document;updateManychanges all matches.matchedCountis how many documents matched the filter;modifiedCountis how many actually changed. Setting a field to the value it already has counts as matched but not modified.- An update with an empty filter
{}andupdateManychanges every document, so double-check filters.
Update operators
| Operator | Effect | Example |
|---|---|---|
$set | Set fields, creating them if needed | { $set: { status: 'shipped' } } |
$unset | Remove fields | { $unset: { tempCode: '' } } |
$inc | Add to a number (negative to subtract) | { $inc: { 'stock.qty': -1 } } |
$mul | Multiply a number | { $mul: { price: 1.1 } } |
$rename | Rename a field | { $rename: { colour: 'color' } } |
$min, $max | Update only if the new value is lower or higher | { $max: { highScore: 980 } } |
$currentDate | Set a field to the current date | { $currentDate: { updatedAt: true } } |
Several operators can be combined in one update, and dot notation updates fields inside embedded documents without replacing the whole sub-document.
Upserts, replacement and findOneAndUpdate
- Upsert: with
{ upsert: true }, if nothing matches, MongoDB inserts a new document built from the filter and the update.$setOnInsertsets fields only when inserting, such ascreatedAt. replaceOne(filter, newDocument)replaces everything except_id. It takes a plain document, not operators.findOneAndUpdateupdates a document and returns it. Pass{ returnDocument: 'after' }to get the new version instead of the old one.
MongoDB CRUD Syntax
Update syntax
db.c.updateOne({ _id: 1 }, { $set: { status: 'paid' } })
db.c.updateMany({ status: 'trial' }, { $set: { status: 'expired' } })
db.c.updateOne({ _id: 1 }, { $inc: { views: 1 }, $currentDate: { updatedAt: true } })
db.c.updateOne({ _id: 1 }, { $unset: { tempToken: '' } })
db.c.updateOne({ email: e }, { $set: { name: n }, $setOnInsert: { createdAt: new Date() } }, { upsert: true })
db.c.replaceOne({ _id: 1 }, { name: 'New', price: 5 })
db.c.findOneAndUpdate({ _id: 1 }, { $inc: { qty: -1 } }, { returnDocument: 'after' })MongoDB CRUD Summary
- updateOne changes the first match; updateMany changes every match.
- Update operators change only the named fields: $set, $unset, $inc, $mul, $rename, $min, $max, $currentDate.
- matchedCount and modifiedCount report what happened.
- upsert: true inserts when nothing matches; $setOnInsert applies only to inserts.
- replaceOne swaps the whole document; findOneAndUpdate returns the document.
MongoDB CRUD Examples
Each example shows the code, the output it produces and what each part does.
$set, $inc and update results
db.products.updateOne({ _id: 2 }, { $set: { price: 26, onSale: false } })
db.products.updateOne({ _id: 2 }, { $set: { price: 26 } })
db.products.updateOne({ _id: 1 }, { $inc: { 'stock.qty': -3 } })
db.products.updateMany({ category: 'kitchen' }, { $mul: { price: 1.1 } })
db.products.find({ _id: { $in: [1, 2, 3] } }, { name: 1, price: 1, 'stock.qty': 1, _id: 0 }){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 0,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 2,
modifiedCount: 2,
upsertedCount: 0
}
[
{ name: 'Mug', price: 9.350000000000001, stock: { qty: 9 } },
{ name: 'Lamp', price: 26, stock: { qty: 0 } },
{ name: 'Kettle', price: 32.989000000000004, stock: { qty: 5 } }
]Line-by-line explanation
- Line 1$set changes the price and adds a new onSale field, leaving the rest untouched.
- Line 2The price is already 26, so the document matches but isn't modified.
- Line 3$inc with a negative number subtracts, here from a nested field.
- Line 4$mul multiplies the price of every kitchen product.
- Line 5Floating-point maths gives long decimals; for money, store whole pennies or Decimal128, covered in the data types lesson.
$unset, $rename, $min, $max and timestamps
db.players.insertOne({ _id: 'ada', score: 120, best: 300, colour: 'red', temp: 'x' })
db.players.updateOne({ _id: 'ada' }, {
$unset: { temp: '' },
$rename: { colour: 'color' },
$max: { best: 250 },
$min: { score: 100 },
$set: { updatedAt: ISODate('2026-03-01T12:00:00Z') }
})
db.players.findOne({ _id: 'ada' })
db.players.updateOne({ _id: 'ada' }, { $max: { best: 410 } })
db.players.findOne({ _id: 'ada' }).best{ acknowledged: true, insertedId: 'ada' }
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
_id: 'ada',
score: 100,
best: 300,
color: 'red',
updatedAt: ISODate('2026-03-01T12:00:00.000Z')
}
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
410Line-by-line explanation
- Line 3$unset removes the field completely.
- Line 4$rename changes a field's name and keeps its value.
- Line 5$max only changes best if the new value is higher; 250 isn't, so best stays 300.
- Line 6$min only changes score if the new value is lower; 100 is, so it updates.
- Line 7In real code, $currentDate or new Date() records when the change happened.
- Line 11A higher value replaces the old best score.
Upserts and findOneAndUpdate
const visit = (page) => db.pageviews.updateOne(
{ page },
{ $inc: { views: 1 }, $setOnInsert: { firstSeen: ISODate('2026-03-01T00:00:00Z') } },
{ upsert: true }
)
visit('/home').upsertedCount
visit('/home').upsertedCount
visit('/shop').upsertedCount
db.pageviews.find({}, { _id: 0 }).sort({ page: 1 })
db.stock.insertOne({ _id: 'MUG', qty: 3 })
db.stock.findOneAndUpdate({ _id: 'MUG', qty: { $gte: 2 } }, { $inc: { qty: -2 } }, { returnDocument: 'after' })
db.stock.findOneAndUpdate({ _id: 'MUG', qty: { $gte: 2 } }, { $inc: { qty: -2 } }, { returnDocument: 'after' })1
0
1
[
{
page: '/home',
firstSeen: ISODate('2026-03-01T00:00:00.000Z'),
views: 2
},
{
page: '/shop',
firstSeen: ISODate('2026-03-01T00:00:00.000Z'),
views: 1
}
]
{ acknowledged: true, insertedId: 'MUG' }
{ _id: 'MUG', qty: 1 }
nullLine-by-line explanation
- Lines 1 to 5An upsert updates the page's counter, or creates it if it doesn't exist yet.
- Line 3$setOnInsert sets firstSeen only when the document is created.
- Lines 6 to 8The first visit to each page inserts (upsertedCount 1); later visits only increment.
- Line 12The filter checks there is enough stock, and the update happens atomically in the same step.
- Line 13Only 1 is left, so nothing matches and findOneAndUpdate returns null: the stock can't go negative.
Common MongoDB CRUD Mistakes
Forgetting the update operator
db.users.updateOne({ _id: 1 }, { name: 'Ada' }) // error: update document requires atomic operatorsdb.users.updateOne({ _id: 1 }, { $set: { name: 'Ada' } })Replacing a nested document by accident
db.users.updateOne({ _id: 1 }, { $set: { address: { city: 'York' } } }) // removes street and postcodedb.users.updateOne({ _id: 1 }, { $set: { 'address.city': 'York' } })Read, change in code, write back
const p = db.products.findOne({ _id: 1 })
db.products.updateOne({ _id: 1 }, { $set: { 'stock.qty': p.stock.qty - 1 } }) // two buyers can overwrite each otherdb.products.updateOne({ _id: 1, 'stock.qty': { $gte: 1 } }, { $inc: { 'stock.qty': -1 } })MongoDB CRUD in Real Projects
Updates in applications
Order systems move orders through statuses with $set and record when with $currentDate. Shopping carts and inventory use $inc with a guard in the filter so stock never goes negative. Analytics counters, rate limiters and "last seen" timestamps use upserts so the first event creates the document. Profile edit forms use $set with dot notation so only the edited fields change.
MongoDB CRUD FAQ
How do I add a new field to every document in a collection?
Run updateMany with an empty filter and $set: db.products.updateMany({}, { $set: { onSale: false } }). To add it only where it's missing, so existing values aren't overwritten, filter on { onSale: { $exists: false } }. Check matchedCount and modifiedCount in the result to confirm how many documents changed.
How do I update a field using the value of another field?
Pass an aggregation pipeline, an array of stages, as the update instead of a plain update document. For example, db.users.updateMany({}, [{ $set: { fullName: { $concat: ['$first', ' ', '$last'] } } }]) builds a field from two others. Inside the pipeline, '$first' means "the value of the first field", which ordinary $set can't do.
What is the difference between updateOne and replaceOne?
updateOne uses operators such as $set and $inc, so it changes only the fields you name. replaceOne takes a plain document and swaps the whole stored document for it, keeping only _id, so any field you leave out is gone. Use replaceOne when your application holds the complete new version of a document, and updateOne for everything else.
Why is modifiedCount 0 when matchedCount is 1?
The filter found the document, but the update didn't change anything, usually because the field already had that value. MongoDB doesn't rewrite a document when the result would be identical. If you expected a change, check the field names and types in your update, since a typo creates or targets a different field.
How do I rename a field in all MongoDB documents?
Use $rename with updateMany: db.products.updateMany({}, { $rename: { colour: 'color' } }). Documents that don't have the old field are left as they are. Remember to update any indexes, validation rules and application code that use the old name.
MongoDB CRUD Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the update to add 1 to the views count.
db.articles.updateOne({ _id: 9 }, { : { views: 1 } })
What's the difference between updateOne and updateMany?
A product has price 10. What are matchedCount and modifiedCount after this?
db.products.updateOne({ _id: 7 }, { $set: { price: 10 } })
What does { upsert: true } do?
How can you stop stock going below zero when two customers buy at once?
MongoDB CRUD Quiz
5 questions to check what you learned.
Which operator removes a field?
- $delete
- $unset
- $remove
- $drop
What does findOneAndUpdate with returnDocument: 'after' return?
- The document after the update
- The old document
- The update result counts
- Nothing
What does $max: { best: 50 } do if best is 80?
- Sets best to 50
- Adds 50
- Removes best
- Leaves best at 80
What does replaceOne keep from the original document?
- All fields
- Nothing
- Nested documents
- Only _id
Why does
{ $set: { address: { city: 'York' } } }lose the street field?- $set can't handle nested documents
- It replaces the whole address sub-document; use 'address.city' to change one field
- It's a bug in MongoDB
- city is a reserved word