MongoDB Update Operators: A Practical Guide
In MongoDB you rarely replace a whole document. Instead you describe the change you want with update operators, the $-prefixed keys that tell the server how to modify matched documents in place. They are used with updateOne, updateMany, findOneAndUpdate, and inside bulk writes.
This guide covers the field operators you reach for daily and the array operators that have the steepest learning curve.
The shape of an update
An update call takes a filter and an update document:
db.users.updateOne(
{ _id: 123 },
{ $set: { status: "active", lastLogin: new Date() } }
)The filter selects documents the same way a find() query does, using the query operators. The second argument must use update operators. If you pass a plain document without operators, like { status: "active" }, MongoDB treats it as a full replacement and deletes every other field. That accident is one of the most common ways to lose data, so always reach for $set.
updateOne changes the first matching document; updateMany changes all of them. Neither does anything if the filter matches nothing, unless you ask for an upsert.
Field operators
$set assigns a value, creating the field if it does not exist. It also reaches into nested paths with dot notation:
db.users.updateOne(
{ _id: 123 },
{ $set: { "address.city": "Zurich" } }
)$unset removes a field entirely. The value you give it is ignored:
db.users.updateOne({ _id: 123 }, { $unset: { tempToken: "" } })$inc adds a number to a field, and accepts negatives to subtract. If the field does not exist, it is created with the increment value:
db.products.updateOne({ _id: 123 }, { $inc: { stock: -1, views: 1 } })$mul multiplies. $min and $max only write the new value if it is lower (or higher) than the current one, which is handy for tracking a running low or high without reading first. $rename changes a field's name. $currentDate sets a field to the server's current date or timestamp.
A subtle but useful operator is $setOnInsert. It only applies when an upsert actually inserts a new document, letting you set creation defaults that should not be touched on subsequent updates:
db.counters.updateOne(
{ _id: "page-views" },
{ $inc: { count: 1 }, $setOnInsert: { createdAt: new Date() } },
{ upsert: true }
)Array operators
Modifying arrays is where MongoDB updates get involved.
$push appends an element. With modifiers it can do more in one operation:
db.posts.updateOne(
{ _id: 1 },
{ $push: { comments: { $each: [c1, c2], $sort: { date: -1 }, $slice: 10 } } }
)Here $each adds multiple elements, $sort reorders the whole array, and $slice trims it to keep only the most recent ten. This combination is a clean way to maintain a capped "latest N" list.
$addToSet is like $push but only adds the element if it is not already present, treating the array as a set:
db.users.updateOne({ _id: 1 }, { $addToSet: { roles: "editor" } })$pull removes every element that matches a condition, and $pullAll removes elements equal to listed values:
db.posts.updateOne({ _id: 1 }, { $pull: { tags: { $in: ["spam", "old"] } } })$pop removes one element from an end: { $pop: { items: 1 } } drops the last, -1 drops the first.
Updating elements inside an array
The hard part is changing an element that is already in the array. Three tools cover the cases.
The positional $ operator updates the first element matched by the query filter. The filter must reference the array, and $ stands in for that matched index:
db.orders.updateOne(
{ _id: 1, "items.sku": "A-100" },
{ $set: { "items.$.quantity": 5 } }
)$ only ever targets the first match. It cannot update multiple matching elements, and it requires the array field to appear in the filter.
The all-positional $[] operator updates every element of the array:
db.inventory.updateOne(
{ _id: 1 },
{ $inc: { "items.$[].count": 1 } }
)Filtered positional $[identifier] updates only elements matching a condition you supply in arrayFilters, and it can update many elements at once, which plain $ cannot:
db.students.updateOne(
{ _id: 1 },
{ $set: { "grades.$[g].status": "pass" } },
{ arrayFilters: [ { "g.score": { $gte: 60 } } ] }
)As a rule: use $ when you matched one element in the filter, $[] to touch all of them, and $[identifier] with arrayFilters when you need to update a specific subset.
Upserts
Passing { upsert: true } makes an update insert a new document if the filter matches nothing. The inserted document is built from the filter's equality conditions plus the update operators, with $setOnInsert values added only on that insert. Upserts are the standard way to write idempotent "create or update" logic, but be careful: a filter using operators like $gt does not contribute those to the inserted document, so the new record may not match the filter you used.
Common mistakes
- Replacing instead of updating. Forgetting
$setand passing a bare document wipes unspecified fields. This is the classic data-loss bug. - Expecting
$to update all matches. The positional$only hits the first matching element. Use$[identifier]witharrayFiltersfor multiple. $addToSetwith$each. To add several unique items, wrap them:{ $addToSet: { tags: { $each: ["a", "b"] } } }. Without$each, you push the array itself as one element.- Upsert filters with operators. Only equality conditions seed the inserted document, so an upsert can create a row that does not satisfy its own filter.
Verifying the result
updateOne and updateMany return matchedCount and modifiedCount. A match with zero modifications usually means the update was a no-op because the value was already what you set. After a bulk change it is worth querying the affected documents to confirm the shape is what you expected. For reading the data back and exploring how array elements changed, a query and analysis tool helps.
Mako connects to MongoDB with AI-powered autocomplete and is suited to writing and explaining the queries you use to verify updates, rather than running schema migrations on the server. For the read side, see our query operators guide. Try it free at mako.ai.
Skip the terminal. Use Mako.
Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.