Updating documents
Updating is the operation people most often get subtly wrong, because the obvious mental model — “find the document and overwrite a field” — is not how MongoDB works by default. An update is two documents: a filter that selects which documents to change, and an update document built from operators that describe the change. The operators are the whole point. They let you bump a number, set a field, remove a field, or push onto an array without touching anything else in the document.
We will keep editing our library members. Every example below shows the document before and after so you can see exactly what each operator did.
updateOne with $set
Section titled “updateOne with $set”$set writes a value into a field. If the field exists it is overwritten; if it does not exist it is created. updateOne applies the change to the first document that matches the filter and stops there. Here we record that Edsger paid off his fines:
db.members.updateOne( { name: "Edsger" }, { $set: { fines: 0 } })const res = await db.collection("members").updateOne( { name: "Edsger" }, { $set: { fines: 0 } });console.log(res.matchedCount, res.modifiedCount);res = db.members.update_one( {"name": "Edsger"}, {"$set": {"fines": 0}},)print(res.matched_count, res.modified_count)res, err := coll.UpdateOne( ctx, bson.M{"name": "Edsger"}, bson.M{"$set": bson.M{"fines": 0}},)if err != nil { return err}fmt.Println(res.MatchedCount, res.ModifiedCount)let res = coll .update_one( doc! { "name": "Edsger" }, doc! { "$set": { "fines": 0 } }, ) .await?;println!("{} {}", res.matched_count, res.modified_count);Before the update, Edsger owed five:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d3", "name": "Edsger", "joined": 2022, "fines": 5 }After it, only the fines field changed:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d3", "name": "Edsger", "joined": 2022, "fines": 0 }The acknowledgement tells you how the operation went. matchedCount is how many documents the filter found; modifiedCount is how many actually changed:
{ "acknowledged": true, "matchedCount": 1, "modifiedCount": 1 }If you run the very same update again, the value is already 0, so matchedCount stays 1 but modifiedCount is 0 — the document matched but nothing needed changing.
$inc, and adding new fields
Section titled “$inc, and adding new fields”$inc adds a number to a field (use a negative number to subtract). It is atomic, which is the right tool for counters and balances. $set doubles as a way to add a field that was never there — here we both charge a late fee and stamp the member with a lastSeen year in one call:
db.members.updateOne( { name: "Linus" }, { $inc: { fines: 3 }, $set: { lastSeen: 2026 } })await db.collection("members").updateOne( { name: "Linus" }, { $inc: { fines: 3 }, $set: { lastSeen: 2026 } });db.members.update_one( {"name": "Linus"}, {"$inc": {"fines": 3}, "$set": {"lastSeen": 2026}},)_, err := coll.UpdateOne( ctx, bson.M{"name": "Linus"}, bson.M{ "$inc": bson.M{"fines": 3}, "$set": bson.M{"lastSeen": 2026}, },)if err != nil { return err}coll.update_one( doc! { "name": "Linus" }, doc! { "$inc": { "fines": 3 }, "$set": { "lastSeen": 2026 } },).await?;Before, Linus had two fines and no lastSeen field:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 2 }After, the count went up by three and a brand-new field appeared:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 5, "lastSeen": 2026}Removing a field with $unset
Section titled “Removing a field with $unset”To delete a field entirely — not set it to null, but make it stop existing — use $unset. The value you give it is ignored; only the field name matters. Here we strip the lastSeen field back off Linus:
db.members.updateOne( { name: "Linus" }, { $unset: { lastSeen: "" } })await db.collection("members").updateOne( { name: "Linus" }, { $unset: { lastSeen: "" } });db.members.update_one( {"name": "Linus"}, {"$unset": {"lastSeen": ""}},)_, err := coll.UpdateOne( ctx, bson.M{"name": "Linus"}, bson.M{"$unset": bson.M{"lastSeen": ""}},)if err != nil { return err}coll.update_one( doc! { "name": "Linus" }, doc! { "$unset": { "lastSeen": "" } },).await?;The lastSeen field is gone again, and nothing else moved:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d1", "name": "Linus", "joined": 2023, "fines": 5 }Working with arrays: $push and $pull
Section titled “Working with arrays: $push and $pull”Documents often hold arrays, and you rarely want to replace the whole array to add or remove one element. $push appends an element; $pull removes every element matching a value. Imagine each member carries a borrowed array of book titles. First we lend a book to Margaret:
db.members.updateOne( { name: "Margaret" }, { $push: { borrowed: "Compilers" } })await db.collection("members").updateOne( { name: "Margaret" }, { $push: { borrowed: "Compilers" } });db.members.update_one( {"name": "Margaret"}, {"$push": {"borrowed": "Compilers"}},)_, err := coll.UpdateOne( ctx, bson.M{"name": "Margaret"}, bson.M{"$push": bson.M{"borrowed": "Compilers"}},)if err != nil { return err}coll.update_one( doc! { "name": "Margaret" }, doc! { "$push": { "borrowed": "Compilers" } },).await?;If the borrowed field did not exist yet, $push creates it as a one-element array:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d2", "name": "Margaret", "joined": 2023, "fines": 0, "borrowed": ["Compilers"]}When the book comes back, $pull removes it by value:
db.members.updateOne( { name: "Margaret" }, { $pull: { borrowed: "Compilers" } })await db.collection("members").updateOne( { name: "Margaret" }, { $pull: { borrowed: "Compilers" } });db.members.update_one( {"name": "Margaret"}, {"$pull": {"borrowed": "Compilers"}},)_, err := coll.UpdateOne( ctx, bson.M{"name": "Margaret"}, bson.M{"$pull": bson.M{"borrowed": "Compilers"}},)if err != nil { return err}coll.update_one( doc! { "name": "Margaret" }, doc! { "$pull": { "borrowed": "Compilers" } },).await?;The element is gone and the array is empty again:
{ "_id": "65f0b3d4e4b0a1c2e4b0a1d2", "name": "Margaret", "joined": 2023, "fines": 0, "borrowed": []}updateMany: changing every match
Section titled “updateMany: changing every match”updateMany is the same call as updateOne, but it applies the change to every document the filter selects. Here we forgive one fine for everyone who currently owes something — note the negative $inc:
db.members.updateMany( { fines: { $gt: 0 } }, { $inc: { fines: -1 } })const res = await db.collection("members").updateMany( { fines: { $gt: 0 } }, { $inc: { fines: -1 } });console.log(res.matchedCount, res.modifiedCount);res = db.members.update_many( {"fines": {"$gt": 0}}, {"$inc": {"fines": -1}},)print(res.matched_count, res.modified_count)res, err := coll.UpdateMany( ctx, bson.M{"fines": bson.M{"$gt": 0}}, bson.M{"$inc": bson.M{"fines": -1}},)if err != nil { return err}fmt.Println(res.MatchedCount, res.ModifiedCount)let res = coll .update_many( doc! { "fines": { "$gt": 0 } }, doc! { "$inc": { "fines": -1 } }, ) .await?;println!("{} {}", res.matched_count, res.modified_count);The counts now reflect everyone who was touched in this single operation:
{ "acknowledged": true, "matchedCount": 2, "modifiedCount": 2 }In Compass: double-click any field value in the Documents tab to edit it inline, or use the Update button after entering a filter to apply an update document to many matches at once.
Tips and gotchas
Section titled “Tips and gotchas”- The most important gotcha: if you pass an update document with no operators — a plain document like the one you would insert — MongoDB treats it as a full replacement and wipes every field you did not include. Almost always you want
$set, not a bare replacement. matchedCountandmodifiedCountcan differ. A document can match the filter yet not be modified, because the new value equals the old one. Re-running an idempotent update is the classic case.updateManyhas no built-in limit — it changes every match. Double-check your filter before running one; a too-broad filter quietly rewrites more documents than you meant.$setcreates missing fields,$unsetremoves them, and$incworks even when the field is absent (it starts from zero). These operators are forgiving about whether the field already exists.