← Back to Index

CRUD
Update

FILE 06_crud_update
TOPIC updateOne · updateMany · replaceOne · Operators · Upsert
LEVEL Foundation
01
Method Overview
Update methods comparison
Overview
MethodAffectsRequires OperatorsStatus
update()First match (or multi: true for all)No — full replacement without operatorsDEPRECATED
updateOne()First match onlyYes — $set, $inc, etc. or aggregation pipelineCURRENT
updateMany()All matching documentsYes — same as updateOne()CURRENT
replaceOne()First match onlyNo — provide full replacement documentCURRENT
findOneAndUpdate()First match onlyYesCURRENT
NOTEAll updates are atomic at the document level. A single document update either fully succeeds or fully fails — no partial writes to a single document. Multi-document operations have no cross-document atomicity.
02
updateOne()
Safe single-document update · upsert · arrayFilters
Single
// Signature
db.collection.updateOne(filter, update, options)

// Basic update with $set
db.users.updateOne(
  { email: "alice@example.com" },
  { $set: { status: "active", updatedAt: new Date() } }
)
// → { matchedCount: 1, modifiedCount: 1, acknowledged: true }
// modifiedCount: 0 if new value equals old value (no actual change)

// Upsert: create if not found
db.users.updateOne(
  { username: "newuser" },
  { $set: { status: "pending" }, $setOnInsert: { createdAt: new Date() } },
  { upsert: true }
)
// $setOnInsert only runs on INSERT (not on update of existing doc)
// → { matchedCount: 0, upsertedCount: 1, upsertedId: ObjectId("...") }

// Sort option (MongoDB 8.0+) — pick which document when multiple match
db.orders.updateOne(
  { status: "pending" },
  { $set: { processing: true } },
  { sort: { orderDate: 1 } }  // update the OLDEST pending order
)

// arrayFilters — update specific array elements by condition
db.students.updateOne(
  { _id: 1 },
  { $set: { "grades.$[elem].passed": true } },
  { arrayFilters: [{ "elem.score": { $gte: 60 } }] }  // only grades >= 60
)

Return Object

FieldMeaning
matchedCount0 or 1 — docs found by filter
modifiedCount0 or 1 — docs actually changed (0 if new value = old value)
upsertedIdThe _id of newly created doc on upsert
DANGERupdateOne() throws an error if you pass a replacement document without update operators. Use replaceOne() for full replacements. Unlike deprecated update(), there is no accidental replacement trap here.
03
updateMany()
Bulk updates · partial failure · idempotency
Bulk
// Signature
db.collection.updateMany(filter, update, options)

// Update all matching documents
db.products.updateMany(
  { category: "electronics" },
  { $set: { onSale: true } }
)

// Update ALL documents in collection (empty filter)
db.users.updateMany({}, { $set: { active: true } })

// Aggregation pipeline update — compute fields from other fields
db.products.updateMany(
  {},
  [{ $set: { totalStock: { $add: ["$warehouseA", "$warehouseB"] } } }]
)
// Pipeline syntax uses [] — allows referencing existing field values

// arrayFilters on many docs — update score elements < 50 to 0
db.users.updateMany(
  {},
  { $set: { "scores.$[elem]": 0 } },
  { arrayFilters: [{ "elem": { $lt: 50 } }] }
)

Partial Failure — Not Atomic

WARNupdateMany() is not atomic across all documents. If it fails midway (network error, type violation), documents already modified stay modified — no rollback. For all-or-nothing, wrap in a multi-document transaction.
// Best practice: use idempotent updates so re-running is safe
// $set is idempotent — running twice gives same result
db.orders.updateMany({ status: "new" }, { $set: { processed: false } })

// $inc is NOT idempotent — re-running doubles the increment
db.items.updateMany({}, { $inc: { views: 1 } })  // dangerous to re-run
04
Field Update Operators
$set · $unset · $inc · $mul · $rename · $min · $max · $currentDate
Operators
OperatorWhat it doesExample
$setSet field value. Adds field if missing.{ $set: { status: "active" } }
$unsetRemove field entirely from document{ $unset: { tempToken: "" } }
$incIncrement (or decrement with negative) a numeric field{ $inc: { views: 1, stock: -1 } }
$mulMultiply field by a factor{ $mul: { price: 1.1 } } (10% increase)
$renameRename a field{ $rename: { "oldName": "newName" } }
$minOnly update if new value is less than current{ $min: { lowestScore: 45 } }
$maxOnly update if new value is greater than current{ $max: { highScore: 99 } }
$currentDateSet field to current date/timestamp{ $currentDate: { updatedAt: true } }
$setOnInsertSets field only when upsert creates a new doc{ $setOnInsert: { createdAt: new Date() } }
// $set creates field if missing, overwrites if exists
db.products.updateOne({ _id: 1 }, { $set: { color: "red" } })

// $unset — value doesn't matter, field is deleted
db.users.updateOne({ _id: 1 }, { $unset: { temporaryToken: "" } })

// $inc — atomic counter (no race condition)
db.posts.updateOne({ _id: postId }, { $inc: { views: 1 } })

// $min/$max — conditional update (highscore board pattern)
db.leaderboard.updateOne(
  { userId: "u1" },
  { $max: { bestScore: 95 } }  // only updates if 95 > current bestScore
)

// $currentDate — set as BSON Date or Timestamp
db.orders.updateOne(
  { _id: orderId },
  { $currentDate: { lastModified: true, processedAt: { $type: "timestamp" } } }
)

// $setOnInsert with upsert — createdAt ONLY set on new documents
db.users.updateOne(
  { username: "bob" },
  {
    $set: { lastSeen: new Date() },
    $setOnInsert: { createdAt: new Date(), role: "user" }
  },
  { upsert: true }
)
// On existing doc: only lastSeen updates; role/createdAt stay unchanged
// On new doc: all three fields are set
05
Array Update Operators
$push · $pull · $addToSet · $pop · positional $ · $[]
Arrays
OperatorWhat it does
$pushAppend element to array (allows duplicates)
$addToSetAppend only if element doesn't already exist (no duplicates)
$pullRemove all elements matching a condition
$popRemove first (-1) or last (1) element
$Positional — update first array element matching the query filter
$[]All positional — update all elements in the array
$[identifier]Filtered positional — update elements matching arrayFilters condition
// $push — add a tag (allows duplicates)
db.posts.updateOne({ _id: 1 }, { $push: { tags: "mongodb" } })

// $push with $each — add multiple elements at once
db.posts.updateOne({ _id: 1 }, { $push: { tags: { $each: ["db", "nosql"] } } })

// $addToSet — add only if not already present (set semantics)
db.users.updateOne({ _id: 1 }, { $addToSet: { skills: "MongoDB" } })

// $pull — remove all matching elements
db.posts.updateOne({ _id: 1 }, { $pull: { tags: "outdated" } })
db.orders.updateOne({ _id: 1 }, { $pull: { items: { qty: { $lt: 1 } } } })

// $pop: 1 = remove last, -1 = remove first
db.lists.updateOne({ _id: 1 }, { $pop: { items: 1 } })   // removes last
db.lists.updateOne({ _id: 1 }, { $pop: { items: -1 } })  // removes first

// $ positional — update the FIRST matching array element
// Document: { scores: [75, 85, 92] }
db.grades.updateOne(
  { _id: 1, scores: 85 },          // query must reference the array field
  { $set: { "scores.$": 90 } }     // $ = index of matched element (85)
)

// $[] — update ALL elements in array
db.students.updateOne(
  { _id: 1 },
  { $inc: { "scores.$[]": 5 } }  // +5 to every score
)

// $[identifier] + arrayFilters — update SPECIFIC elements
db.students.updateOne(
  { _id: 1 },
  { $set: { "grades.$[elem].passed": true } },
  { arrayFilters: [{ "elem.score": { $gte: 60 } }] }  // only grades >= 60
)
06
Edge Cases
Replacement trap · null parent · NaN · _id · non-atomicity
Edge Cases

The Replacement Trap — update() without Operators

// WRONG — replaces entire document, ALL other fields deleted!
db.users.update({ name: "Alice" }, { status: "active" })
// Document becomes: { _id: ObjectId("..."), status: "active" }
// email, age, address — all GONE

// CORRECT — only updates the specified field
db.users.updateOne({ name: "Alice" }, { $set: { status: "active" } })

// replaceOne() — intentional full replacement (keeps _id)
db.users.replaceOne(
  { name: "Alice" },
  { name: "Alice", status: "active", email: "alice@example.com" }
)
// Replaces everything except _id — use when you intend full replacement

Cannot Update _id

// _id is immutable — attempting to change it throws
db.users.updateOne({ _id: 1 }, { $set: { _id: 2 } })
// MongoServerError: Performing an update on the path '_id' would modify
// the immutable field '_id'

Nested Field Update on null Parent

// Document: { _id: 1, address: null }
db.users.updateOne({ _id: 1 }, { $set: { "address.city": "Mumbai" } })
// Error: "Cannot create field 'city' in element {address: null}"

// Fix: set parent to an object first, then set nested field
db.users.updateOne({ _id: 1 }, { $set: { address: {} } })
db.users.updateOne({ _id: 1 }, { $set: { "address.city": "Mumbai" } })

NaN Behavior with $inc

// $inc on a NaN field stays NaN — NaN + anything = NaN
db.data.updateOne({ _id: 1 }, { $inc: { value: 1 } })
// If value was NaN, it remains NaN after this update

// Fix: $set to a real number first
db.data.updateOne({ _id: 1 }, { $set: { value: 0 } })
db.data.updateOne({ _id: 1 }, { $inc: { value: 1 } })
// → value is now 1

updateMany() Partial Failure

// updateMany() stops on error — no rollback for already-modified docs
// Example: error at doc 50 means docs 1-49 are updated, 51+ are not
// No automatic rollback — must handle manually or use a transaction

// Solution: wrap in multi-document transaction for atomicity
const session = db.getMongo().startSession()
session.startTransaction()
try {
  db.getSiblingDB("mydb").orders.updateMany(
    { status: "pending" },
    { $set: { status: "processing" } },
    { session }
  )
  session.commitTransaction()
} catch(e) {
  session.abortTransaction()  // rolls back ALL changes
} finally {
  session.endSession()
}
WARNA { status: null } filter in updateMany() targets documents where status is explicitly null AND documents where status is missing — this is the null/missing gotcha that can accidentally modify more docs than expected.