skip to content

In MongoDB, what is the difference between updateOne with $set and replaceOne?

level: juniorimportance: must knowfreq 78%

answer

  1. One changes fields, one swaps documents
  2. Think about the fields you did not mention
  3. Operators versus a plain document
  4. Dot notation edits one nested field
  5. Omitted fields survive one call and vanish in the other

basics

~20 s

updateOne with $set changes only the named fields and leaves every other field intact. replaceOne swaps the whole document for the one you pass, so any field the replacement omits disappears. _id cannot be changed either way.

solid answer

~40 s

`updateOne(filter, update)` takes an **update document made of operators** — `$set`, `$unset`, `$inc`, `$push` and so on — and applies exactly those field-level changes; everything else in the stored document is untouched. `$set` also accepts dot notation (`"profile.city"`), so you can rewrite one field deep inside an embedded document without touching its siblings, and it creates missing paths on the way. `replaceOne(filter, replacement)` takes a plain document with **no** operators and substitutes it for the matched document wholesale — fields absent from the replacement are gone. The two APIs reject each other's input: a plain document passed to `updateOne` is refused because the update must contain atomic operators, and an operator document passed to `replaceOne` is refused too. `_id` is immutable, so a replacement either omits `_id` or repeats the same value.

code

javascript · 5 lines
javascript
// partial update: only profile.city changes
db.users.updateOne({ _id: 1 }, { $set: { "profile.city": "Berlin" } })

// whole-document replacement: every other field is dropped
db.users.replaceOne({ _id: 1 }, { name: "Ada" })

go deeper

for a junior

Be ready to say, in one sentence, that $set edits named fields while replaceOne swaps the entire document, and to write both calls correctly in the shell.

for a middle

Explain why the API rejects a bare document in updateOne, what dot notation does to missing parent paths, and the difference between matchedCount and modifiedCount.

for a senior

Show why read-modify-write plus replaceOne loses concurrent field changes and unknown fields added by a newer service version, and argue for field-level operators as the default.

for a principal

Own the convention across services: which write paths are allowed to replace whole documents, how that interacts with rolling deploys where two schema versions run at once, and how it is enforced in review.

## The two write shapes MongoDB gives you two ways to change an existing document, and interviewers ask about the difference because choosing the wrong one silently destroys data. `db.collection.updateOne(filter, update, options)` performs a **targeted modification**. The `update` argument is not a document you want stored — it is a set of instructions, each keyed by an update operator: `$set`, `$unset`, `$inc`, `$mul`, `$min`, `$max`, `$rename`, `$currentDate`, plus the array operators `$push`, `$addToSet`, `$pull` and `$pop`. The server applies those instructions to the matched document and leaves every field the instructions do not mention exactly as it was. `db.collection.replaceOne(filter, replacement, options)` performs a **whole-document substitution**. The `replacement` argument is a normal document with no operators, and after the write the stored document *is* that document (plus its `_id`). Every field you did not include is gone. ## Update operators are mandatory for updateOne A very common beginner mistake is calling `db.users.updateOne({ _id: 1 }, { name: "Ada" })`. That is rejected: the update document must contain atomic operators. The correct form is `{ $set: { name: "Ada" } }`. Older MongoDB shells did allow a `db.collection.update()` call with a bare document, and it behaved as a *replacement* — which is exactly the trap. The modern CRUD API makes the two intentions explicit, which is why the legacy `update()`/`remove()` helpers were dropped from current drivers in favour of `updateOne`/`updateMany`/`replaceOne`/`deleteOne`/`deleteMany`. ## What $set actually does `$set` sets a field to a value, creating the field if it is absent. With dot notation it reaches inside embedded documents and arrays: `{ $set: { "address.city": "Berlin" } }` rewrites just that one field and, if `address` does not exist, creates it as an embedded document. `{ $set: { "items.0.qty": 3 } }` targets a specific array index. Its mirror image is `$unset`, which removes a field entirely — the value you give `$unset` is ignored, so `{ $unset: { nickname: "" } }` is idiomatic. One detail worth knowing: if a document already has the exact value you are setting, the write matches but does not modify. The result object distinguishes `matchedCount` from `modifiedCount`, and a `modifiedCount` of 0 with `matchedCount` of 1 means "found it, nothing changed" — not a failure. ## _id is immutable Neither operation may change `_id`. A `$set` on `_id` is an error, and a `replaceOne` whose replacement carries a different `_id` is an error too. Omitting `_id` from the replacement is fine — the existing one is kept. ## Why replaceOne is the riskier choice `replaceOne` is usually the tail end of a read-modify-write cycle: the application loads a document, mutates an object in memory, and writes the whole thing back. That pattern has two problems. First, it is **lossy across schema change**: if another version of the service (or a background job) added a field the reader's model does not know about, serializing the model back over the document deletes that field. Second, it is **lossy under concurrency**: between the read and the write, another client may have changed a different field of the same document, and the replacement overwrites their change with the stale value the reader loaded. Field-level operators avoid both, because two clients touching different fields of the same document do not overwrite each other, and a counter changed with `$inc` never depends on the value the client last read. The practical rule: use `$set` and friends by default, and reach for `replaceOne` only when you genuinely mean "this document should now be exactly this" — for example when re-materializing a document from an authoritative source. ## Multi-document and bulk variants `updateMany` applies the same operator document to every match; there is deliberately no `replaceMany`, because replacing many documents with one identical body is almost never intended. Both `updateOne` and `replaceOne` accept `{ upsert: true }`, and both are available as operations inside `bulkWrite`. ## Updates written as an aggregation pipeline Since MongoDB 4.2, the update argument may also be an aggregation pipeline — an array such as `[ { $set: { total: { $add: ["$price", "$tax"] } } } ]`. That form lets a field be computed from other fields of the same document in one server-side write, which plain update operators cannot do. It is still an update, not a replacement: stages you do not write leave the rest of the document alone.

  • What happens if you pass a plain document with no operators to updateOne?
    The server rejects it: an update document must contain atomic operators. It is not silently treated as a replacement. That strictness exists because the legacy `update()` helper *did* treat a bare document as a whole-document replacement, and people lost fields that way. If replacement is what you want, call `replaceOne` explicitly.
  • How do you remove a field from a document, and what does $unset return in the result?
    Use `{ $unset: { nickname: "" } }` — the value is ignored, only the key matters. If the field was present, the document is modified and `modifiedCount` is 1; if it was already absent, the document still matches but nothing changes, so `matchedCount` is 1 and `modifiedCount` is 0. `$unset` on a nonexistent path is not an error.
  • When is replaceOne genuinely the right call?
    When the new document is authoritative in full — for instance re-projecting a record from an upstream system of record, or rewriting a cached materialization. In those cases the intent really is "this document should now be exactly this", and any field not present upstream should disappear. Everywhere else, field-level operators are safer.

saying these in an interview costs you the question

  • Thinks updateOne without operators just replaces the document
  • Believes replaceOne merges the replacement into the existing document
  • Assumes $set on a nested path wipes the whole embedded document
  • Claims _id can be changed by supplying a new one in a replacement
  • Treats modifiedCount 0 as an error rather than 'value already equal'

context