skip to content

Writes & Update Operators

Inserting, updating and deleting without read-modify-write races, including the operators that mutate arrays in place. Interviewers like 'update the third matching array element' because it forces you to know positional operators.

part ofMongoDBoverview, primer and where to startread it →
on this pageshow

questions

6

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

open as a page

How do the positional $ operator, $[] and arrayFilters differ when updating arrays?

level: middleimportance: must knowfreq 68%

basics

~20 s

The positional $ updates only the first array element matched by the query filter. $[] updates every element unconditionally. $[identifier] with arrayFilters updates exactly the elements matching the given condition, and is the only form that can update several specific elements.

open as a page

What does upsert: true do in updateOne, and what does $setOnInsert add?

level: middleimportance: must knowfreq 64%

basics

~20 s

With upsert: true, a matching document is updated normally; if none matches, MongoDB inserts a new document built from the filter's equality conditions plus the update's operators. $setOnInsert supplies fields that apply only on that insert and are ignored when a document matched.

open as a page

What is the difference between the $push and $addToSet update operators?

level: juniorimportance: should knowfreq 62%

basics

~10 s

$push always appends the value to the array, so duplicates accumulate. $addToSet appends only if an equal value is not already present, giving set semantics. Both create the array when the field is missing.

open as a page

When would you use findOneAndUpdate instead of updateOne in MongoDB?

level: middleimportance: should knowfreq 55%

basics

~20 s

Use findOneAndUpdate when you need the document back as part of the write — either its pre-update state or the post-update state via returnDocument: "after" — or when a sort must decide which single document is modified. updateOne only returns counts.

open as a page

A bulkWrite of 10,000 operations fails partway with a duplicate-key error — what has been applied, and how does ordered versus unordered change it?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Everything already applied stays applied — a bulkWrite is not atomic across its operations. Ordered (the default) executes in array order and stops at the first error, leaving the tail untouched. Unordered continues past failures and reports all errors together.

open as a page