#updateOne and updateMany with $set
An update has two parts: a filter that picks documents (exactly the same language as find) and an update document of update operators that say what to change. updateOne(filter, update) changes the first match; updateMany(filter, update) changes every match. The most common operator is $set, which sets fields to values — adding them if they don't exist.
The mental model: an update is a set of edit instructions applied to each matching document on the server, not a "read, change in your code, write back". That matters because each single-document update is atomic: two clients doing $inc on the same counter can't lose each other's increments, and nobody can see a half-applied update. Both methods return a result with matchedCount (how many documents the filter found) and modifiedCount (how many actually changed) — and those two numbers are not always the same.
Every topic in this chapter starts from the same small users collection, reset before each topic:
db.users.insertMany([
{ _id: 1, name: "Alice", age: 28, city: "Toronto", tags: ["vip"],
activities: [ { type: "swimming", status: "done" }, { type: "chess", status: "pending" }, { type: "hiking", status: "pending" } ] },
{ _id: 2, name: "Bob", age: 45, city: "New York",
activities: [ { type: "golf", status: "done" } ] },
{ _id: 3, name: "Chandra", age: 52, city: "Mumbai",
activities: [ { type: "swimming", status: "pending" }, { type: "cricket", status: "done" } ] },
{ _id: 4, name: "Dana", age: 35, city: "Los Angeles" },
{ _id: 5, name: "Elif", age: 41, city: "Los Angeles", activities: [] }
]).insertedIds
db.users.updateOne({ name: "Alice" }, { $set: { age: 11 } })
db.users.updateOne({ name: "Alice" }, { $set: { age: 11 } })
db.users.findOne({ _id: 1 }, { name: 1, age: 1 }){ '0': 1, '1': 2, '2': 3, '3': 4, '4': 5 }
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 0,
upsertedCount: 0
}
{ _id: 1, name: 'Alice', age: 11 }- Five users go in. Alice has three activities, Bob one, Chandra two; Dana has no
activitiesfield and Elif has an empty array — later topics rely on those differences. - My notes' first update: "update user by name, set age to 11 using
$set". The filter{ name: "Alice" }finds one document and$setchangesagefrom 28 to 11:matchedCount: 1, modifiedCount: 1. - Running the same update again finds Alice (
matchedCount: 1) but changes nothing (modifiedCount: 0): her age is already 11, and MongoDB detects that the update is a no-op and doesn't write. That's how you tell "not found" from "already in that state". insertedId: nullandupsertedCount: 0belong to upserts; with a normal update they're always null and 0.findOneconfirms the stored value. Onlyagechanged:$settouches only the fields it names, and every other field — including the wholeactivitiesarray — is untouched.
My notes' multi-document updates — everyone over 40, a $in on city, and a filter with $and:
db.users.updateMany({ age: { $gt: 40 } }, { $set: { ageGroup: "40+" } })
db.users.updateMany({ city: { $in: ["New York", "Los Angeles"] } }, { $set: { status: "active" } })
db.users.updateMany({ $and: [ { city: "Los Angeles" }, { age: { $lt: 40 } } ] }, { $set: { promo: "LA-under-40" } })
db.users.find({}, { _id: 0, name: 1, age: 1, city: 1, ageGroup: 1, status: 1, promo: 1 }){
acknowledged: true,
insertedId: null,
matchedCount: 3,
modifiedCount: 3,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 3,
modifiedCount: 3,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
[
{ name: 'Alice', age: 28, city: 'Toronto' },
{
name: 'Bob',
age: 45,
city: 'New York',
ageGroup: '40+',
status: 'active'
},
{ name: 'Chandra', age: 52, city: 'Mumbai', ageGroup: '40+' },
{
name: 'Dana',
age: 35,
city: 'Los Angeles',
status: 'active',
promo: 'LA-under-40'
},
{
name: 'Elif',
age: 41,
city: 'Los Angeles',
ageGroup: '40+',
status: 'active'
}
]- Alice's age was put back to 28 off-page first. My notes set a name here; a label field is the more realistic case.
updateMany({ age: { $gt: 40 } }, …)then matches Bob (45), Chandra (52) and Elif (41) and adds a new fieldageGroupto each:matchedCount: 3, modifiedCount: 3.$setcreates a field that doesn't exist. - The
$inupdate — "city is New York or Los Angeles → status active" — matches Bob, Dana and Elif. - The
$andfilter picks users in Los Angeles and under 40: only Dana. (Here the implicit form{ city: "Los Angeles", age: { $lt: 40 } }would do the same;$andis required only for repeated keys, as in Logical operators.) - The final
findshows the result: fields appear only on the documents each update matched. Alice and Chandra have nostatus; only Dana haspromo. - The filter is the whole safety story:
updateManywith a filter that's too broad silently changes more documents than you meant. Check it withcountDocuments(filter)first.
My notes: "add a hobbies array field to all documents", and "update all records where target >= 200, add a new field":
db.users.updateMany({}, { $set: { hobbies: [] } })
db.sales.updateMany({ target: { $gte: 200 } }, { $set: { reviewed: true } })
db.sales.find({}, { _id: 0, item: 1, target: 1, reviewed: 1 })
db.users.updateOne({ _id: 1 }, { $set: { "address.city": "Toronto", "address.zip": "M5H" } })
db.users.findOne({ _id: 1 }, { _id: 0, address: 1, hobbies: 1 }){
acknowledged: true,
insertedId: null,
matchedCount: 5,
modifiedCount: 5,
upsertedCount: 0
}
{
acknowledged: true,
insertedId: null,
matchedCount: 2,
modifiedCount: 2,
upsertedCount: 0
}
[
{ item: 'laptops', target: 200, reviewed: true },
{ item: 'phones', target: 220, reviewed: true },
{ item: 'books', target: 80 }
]
{
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
{ hobbies: [], address: { city: 'Toronto', zip: 'M5H' } }- An empty filter
{}matches every user, soupdateMany({}, { $set: { hobbies: [] } })gives all five an emptyhobbiesarray:matchedCount: 5, modifiedCount: 5. - On a
salescollection (laptops target 200, phones 220, books 80),{ target: { $gte: 200 } }matches laptops and phones, which getreviewed: true. Books is untouched, so it has noreviewedfield at all. $setwith dot notation reaches into embedded documents. Alice had noaddress, so MongoDB creates the embedded document and puts both fields in it:address: { city: 'Toronto', zip: 'M5H' }.- Dot notation in
$setnever replaces the whole embedded document —$set: { "address.zip": "M5V" }later would change only the zip.$set: { address: { zip: "M5V" } }(no dot) would replace the wholeaddressand losecity.