Chapter 05 · Updates, Array Mutations, Upserts, findAndModify, and Bulk Writes

$set, $unset, $inc, $mul, $min, $max, $rename, and Replacement Updates

Use MongoDB update operators and replacement writes safely by reading acknowledged result metadata, comparing exact before/after state, and recognizing when a local field mutation is safer than whole-document replacement.

Intermediate90–120 minutesMutation semantics + failure-aware labMongoDB 8.3.8 · mongosh 2.10.0 · PyMongo 4.17Last reviewed: September 2026

Learning outcomes

AtlasMart needs to change product state without accidentally erasing unrelated fields or misreading a successful no-op as a failed match. MongoDB exposes two distinct mutation styles: operator updates modify selected paths, while replacement writes replace the document body. The difference is architectural, not cosmetic: the first preserves unspecified fields; the second intentionally discards them.

01

Explain what $set, $unset, $inc, $mul, $min, $max, and $rename do to an existing document and what they do when a field is absent or incompatible.

02

Interpret acknowledged, matchedCount, and modifiedCount without conflating no match with a matched no-op.

03

Contrast updateOne() operator documents with replaceOne() replacement documents and the immutable _id boundary.

04

Demonstrate a type error and a destructive replacement mistake, then verify the repaired state.

05

Choose path-level updates versus whole-document replacement from ownership and schema-contract reasoning.

Reproducible lab baseline · reviewed 2 September 2026

Mandatory examples use MongoDB Community Server 8.3.8 in the pinned mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim image, a disposable standalone mongod published only on 127.0.0.1:27037, and mongosh 2.10.0. PyMongo examples target the 4.17 line where driver behavior matters. Authentication and TLS are intentionally disabled only inside this isolated loopback lab; do not copy that posture to a shared or remotely reachable server. Default standalone read/write concern semantics are used, no replica-set/sharding guarantees are claimed, and the reset path removes atlasmart-mongo-ch05-l1. The lesson uses one document at a time, so the write itself is atomic at the MongoDB document boundary; it does not protect invariants that span multiple documents.

1. Update operators express intent at field-path granularity

$set assigns a value and creates the field if the path can be created. $unset removes a field. Numeric operators such as $inc and $mul compute a new value from the current value, while $min and $max conditionally replace only when the proposed value compares lower or higher. $rename moves a field name. These are server-side mutations: the client does not need to fetch the document, calculate a new copy, and race another writer.

Operator AtlasMart use Important boundary
$set Change status or nested campaign metadata Can create a missing field; setting the same value can yield matched=1, modified=0.
$unset Remove obsolete optional metadata Removing a missing field is a no-op.
$inc / $mul Adjust counters, stock, or numeric factors $inc on an explicit null value errors; type contracts matter.
$min / $max Keep a floor/ceiling or extreme observed value Comparison follows BSON comparison semantics; do not mix incompatible domain types casually.
$rename Migrate a field name It is a data mutation, not an application alias; readers must tolerate rollout order.

2. Acknowledged results describe what MongoDB observed

An acknowledged update result separates at least two questions. matchedCount asks whether the filter selected a document. modifiedCount asks whether the persisted document changed. A matched no-op commonly returns matchedCount: 1 and modifiedCount: 0. A miss returns both as zero. Production code should decide which distinction matters to the business rule instead of treating every zero modification as an error.

Atomicity scope

Each single-document update is atomic for that document. That means concurrent readers do not observe a half-applied combination of operators on the same document. It does not make a read-then-update sequence atomic, and it does not enforce cross-document inventory/payment invariants.

3. Replacement is deliberately destructive

updateOne() expects update operators or an allowed aggregation update pipeline. To replace the document body, use replaceOne(). The replacement cannot contain update operators. On a matched replacement, MongoDB preserves the immutable _id value when it is omitted; if you include _id, it must equal the existing value. Every other omitted field disappears. That makes replacement appropriate when one service owns the entire document representation and dangerous when multiple writers own different fields.

Deliberately wrong approach

Reading a product, constructing a partial object such as {name, priceCents}, and passing it to replaceOne() silently removes stock, category, metrics, schema-version, and any other omitted fields. The repair is either a complete replacement built from an explicit canonical model or a narrow operator update that changes only the paths this writer owns.

4. Run the operator-versus-replacement lab

bash · start the disposable MongoDB 8.3.8 lab
docker rm -f atlasmart-mongo-ch05-l1 2>/dev/null || truedocker run --name atlasmart-mongo-ch05-l1 -p 127.0.0.1:27037:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimmongosh "mongodb://127.0.0.1:27037/atlasmart?directConnection=true" --quiet --eval 'printjson({version:db.version(), hello:db.hello().isWritablePrimary})' 
javascript · operator results, no-op evidence, replacement loss, and type error
db.product_mutations.drop();db.product_mutations.insertOne({  _id:"sku-cam-100", name:"AtlasCam 100", priceCents:12500, stock:12,  reorderPoint:5, metrics:{views:100, purchases:4}, status:"active",  legacyCategory:"cameras", optionalNote:null});const before=db.product_mutations.findOne({_id:"sku-cam-100"}); printjson({before});let r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$set:{"metrics.lastCampaign":"fall-2026",status:"featured"}});printjson({step:"$set",acknowledged:r.acknowledged,matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,doc:db.product_mutations.findOne({_id:"sku-cam-100"})});r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$unset:{optionalNote:""},$inc:{stock:-2,"metrics.purchases":1}});printjson({step:"$unset+$inc",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,doc:db.product_mutations.findOne({_id:"sku-cam-100"})});r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$mul:{priceCents:1.1},$min:{reorderPoint:4},$max:{"metrics.views":120}});printjson({step:"$mul+$min+$max",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,doc:db.product_mutations.findOne({_id:"sku-cam-100"})});r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$rename:{legacyCategory:"category"}});printjson({step:"$rename",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,doc:db.product_mutations.findOne({_id:"sku-cam-100"})});// No-op is different from no match: matchedCount 1, modifiedCount 0.r=db.product_mutations.updateOne({_id:"sku-cam-100"},{$set:{status:"featured"}});printjson({step:"no-op",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount});// Replacement rewrites every field except immutable _id. Omitting category here really removes it.r=db.product_mutations.replaceOne({_id:"sku-cam-100"},{name:"AtlasCam 100",priceCents:13750,stock:10,status:"featured",schemaVersion:2});printjson({step:"replaceOne",matchedCount:r.matchedCount,modifiedCount:r.modifiedCount,after:db.product_mutations.findOne({_id:"sku-cam-100"})});// Deliberate type error: $inc on null is invalid.db.product_mutations.insertOne({_id:"bad-null",counter:null});try{db.product_mutations.updateOne({_id:"bad-null"},{$inc:{counter:1}})}catch(e){printjson({expectedError:e.codeName||e.name,message:e.message})}
python · PyMongo result metadata uses the same conceptual distinctions
from pymongo import MongoClientclient=MongoClient("mongodb://127.0.0.1:27037/?directConnection=true", serverSelectionTimeoutMS=3000)coll=client.atlasmart.product_mutationsres=coll.update_one({"_id":"sku-cam-100"},{"$set":{"status":"featured"}})print({"acknowledged":res.acknowledged,"matched":res.matched_count,"modified":res.modified_count})client.close()

Expected evidence

You should observe each update returning acknowledged result metadata, the same _id across the replacement, the no-op reporting one match and zero modifications, and the explicit null counter rejecting $inc. Exact error text can vary by server/driver version, so verify the error category and unchanged document rather than matching a brittle string.

Verification checklist

  • The container is bound only to loopback on port 27037.
  • Each update prints both result metadata and a fresh server-side document read.
  • The no-op is distinguishable from a missing document.
  • The replacement visibly removes fields not included in its replacement body.
  • The null-counter example errors without mutating the field.
  • Cleanup removes the disposable server.

Check your understanding

  1. Why can matchedCount be 1 while modifiedCount is 0?
  2. Why is replaceOne riskier than $set for a shared document?
  3. Does $inc protect inventory from every oversell race?
  4. What does an acknowledged result prove?
  5. When is replacement appropriate?
Review the answers

The filter found a document, but the requested mutation produced the same stored value or otherwise made no change.

Replacement discards every non-_id field omitted from the replacement body, so another writer's fields can be erased.

No. It is atomic within one document, but the filter must encode the business precondition, and cross-document invariants need a stronger design.

It proves the server acknowledged the operation under the configured write concern and reports match/change metadata; it does not by itself prove a higher-level business invariant.

When one writer owns the full canonical document representation and intentionally wants unspecified fields removed.

bash · cleanup/reset
docker rm -f atlasmart-mongo-ch05-l1

The next lesson keeps single-document atomicity but moves the mutation target inside arrays, where positional binding and duplicate semantics create new failure modes.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.