← Back

Field Update
Operators

FILE 12_update_operators
TOPIC $set · $unset · $inc · $mul · $rename · $min · $max · $currentDate · $setOnInsert
LEVEL Foundation
01
Overview
All field update operators at a glance
Reference

Field update operators are the building blocks of any MongoDB update. They are placed inside an update document (alongside updateOne, updateMany, findOneAndUpdate, etc.) and tell MongoDB how to modify each field — as opposed to replacing the whole document.

Operator Purpose Creates if Missing Errors on Wrong Type
$set Assign value (create or overwrite) Yes No
$unset Remove field entirely No (no-op) No
$inc Add / subtract a numeric value Yes — at increment value Yes (non-numeric)
$mul Multiply numeric field by a factor No — SILENT no-op! Yes (non-numeric)
$rename Rename a field, preserving its value No No
$min Update only if new value is LOWER than current Yes Depends on comparison
$max Update only if new value is HIGHER than current Yes Depends on comparison
$currentDate Set to current server timestamp Yes No
$setOnInsert Set fields only when an upsert causes an insert (not an update) N/A No
NOTE All updates are ATOMIC at the document level. Multiple operators can be freely combined in a single update call — MongoDB applies all of them in one operation.
CRITICAL The same field cannot appear in two operators in one update call. { $set: { age: 30 }, $inc: { age: 1 } } throws a conflict error at the server. If you need both effects, split into two separate update operations.
// Combining multiple operators in a single atomic update — all or nothing
db.users.updateOne(
  { _id: ObjectId("64a1f3e2b5c0d12345678901") },
  {
    $set:         { status: "active", "profile.verified": true },
    $inc:         { loginCount: 1 },
    $currentDate: { lastLogin: true }
    // All three changes are applied atomically — no partial writes
  }
);
// $setOnInsert — only fires during the INSERT half of an upsert
db.users.updateOne(
  { email: "bob@example.com" },           // filter
  {
    $set:         { lastSeen: new Date() },  // always runs
    $setOnInsert: { createdAt: new Date(),   // only on new document creation
                    role: "viewer" }
  },
  { upsert: true }
);
// If doc exists → only $set runs (lastSeen updated)
// If doc missing → $set + $setOnInsert both run (new doc with createdAt + role)
02
$set / $unset
Create, overwrite, and remove fields
Core

$set

The single most-used update operator. $set writes a value to a field — creating it if absent, overwriting it if present. It works at any nesting depth and on any BSON type.

  • Creates field if missing; overwrites silently if it already exists
  • Dot notation for nested fields: { $set: { "address.city": "Mumbai" } }
  • Set a specific array element by index: { $set: { "scores.0": 95 } }
  • Setting a field to null is not the same as unsetting — the field remains in the document with a null value and will appear in queries
DANGER Dot notation vs object literal. { $set: { "address.city": "Mumbai" } } safely changes only city while preserving sibling fields (zip, country, etc.). { $set: { address: { city: "Mumbai" } } } replaces the entire address subdocument, silently deleting every other field inside it.
// Starting document:
// { _id: 1, name: "Alice", age: 28, address: { city: "Delhi", zip: "110001", country: "IN" } }

// ✓ SAFE — dot notation preserves sibling fields
db.users.updateOne(
  { _id: 1 },
  { $set: { "address.city": "Mumbai", age: 29 } }
);
// Result: { _id: 1, name: "Alice", age: 29, address: { city: "Mumbai", zip: "110001", country: "IN" } }

// ✗ DESTRUCTIVE — object literal replaces entire subdocument
db.users.updateOne(
  { _id: 1 },
  { $set: { address: { city: "Mumbai" } } }
);
// Result: { _id: 1, name: "Alice", age: 29, address: { city: "Mumbai" } }
// zip and country are permanently gone

// Set a specific array element by index
// Before: { _id: 5, scores: [75, 82, 90] }
db.students.updateOne(
  { _id: 5 },
  { $set: { "scores.0": 95 } }
);
// After: { scores: [95, 82, 90] }  — only index 0 changed

// null ≠ unset — field still exists
db.users.updateOne({ _id: 1 }, { $set: { middleName: null } });
// { ..., middleName: null }  — field IS present, just null-valued
// { middleName: { $exists: true } } WILL match this document

$unset

Removes a field from the document entirely. The value supplied inside $unset is completely ignored by MongoDB — convention is to use "", but 1 and null work identically.

  • Value in the $unset expression is irrelevant"", 1, and null are all equivalent
  • Silent no-op if the field does not exist; never throws an error for missing fields
  • On an array index: sets element to null leaving a hole — does not collapse the array; use $pull to actually remove an element
  • Cannot unset _id
WARN { $unset: { "scores.1": "" } } does not remove index 1 from the array. It sets that slot to null, yielding [80, null, 90] — a sparse array with a hole. Use $pull to remove an element by value, or the aggregation pipeline update with $filter to remove by index.
// Before: { _id: 2, name: "Bob", tempToken: "abc123", age: 34 }

// Remove a field — the value ("") is ignored by MongoDB
db.users.updateOne(
  { _id: 2 },
  { $unset: { tempToken: "" } }
);
// After: { _id: 2, name: "Bob", age: 34 }  — tempToken is gone

// Remove multiple fields at once
db.users.updateOne(
  { _id: 2 },
  { $unset: { tempToken: "", resetCode: "", sessionData: "" } }
);

// No-op: nonExistent field — no error
db.users.updateOne({ _id: 2 }, { $unset: { nonExistentField: "" } });
// Document unchanged, operation succeeds silently

// Array index — sets null, does NOT remove
// Before: { _id: 3, scores: [80, 90, 70] }
db.students.updateOne({ _id: 3 }, { $unset: { "scores.1": "" } });
// After:  { _id: 3, scores: [80, null, 70] }  — hole left behind

// Correct removal: use $pull to remove by value
db.students.updateOne({ _id: 3 }, { $pull: { scores: 90 } });
// After:  { _id: 3, scores: [80, 70] }  — array compacted
03
$inc / $mul
Atomic arithmetic on numeric fields
Numeric

$inc

Increments or decrements a numeric field by the given amount. A positive value increments; a negative value decrements. When the field is missing, $inc creates it at the delta value (equivalent to starting from zero).

  • Positive value = increment; negative value = decrement
  • Creates the field at the increment value if the field is missing
  • Errors on non-numeric fields (cannot increment a string, array, etc.)
  • Works on 32-bit int, 64-bit long, double, and Decimal128
  • Use Decimal128 for monetary values to avoid float precision drift
TIP Concurrent safety: multiple simultaneous $inc calls on the same document are serialized by MongoDB's document-level locking. No lost updates occur — this is the core reason to use atomic operators instead of a read-then-write pattern from application code.
WARN To guard against decrementing below zero (e.g. inventory going negative), include the decrement magnitude as a floor guard in the filter: { quantity: { $gte: 1 } }. If the filter fails to match, MongoDB returns matchedCount: 0 — no update, no error, no negative value.
// Simple increment and decrement in one atomic call
db.products.updateOne(
  { _id: "PROD-01" },
  { $inc: { stock: -1, totalSold: 1 } }
  // stock decremented, totalSold incremented — atomically
);

// Guard against negative stock — filter acts as a gate
const result = db.products.updateOne(
  { _id: "PROD-01", stock: { $gte: 1 } },   // only match if stock >= 1
  { $inc: { stock: -1, totalSold: 1 } }
);
if (result.matchedCount === 0) {
  // Stock was already 0 — handle out-of-stock in app logic
}

// Creating a field via $inc (field does not exist yet)
// Before: { _id: 7, slug: "intro-to-mongodb" }  — no "views" field
db.articles.updateOne(
  { slug: "intro-to-mongodb" },
  { $inc: { views: 1 } }
);
// After: { ..., views: 1 }  — field created, initialized to 1 (0 + 1)

// Page view counter with timestamp
db.articles.updateOne(
  { slug: "intro-to-mongodb" },
  {
    $inc:         { views: 1 },
    $currentDate: { lastViewed: true }
  }
);

// Float precision — avoid for money
// 0.1 + 0.2 !== 0.3 in IEEE 754 floating point
// Use NumberDecimal for financial values:
db.accounts.updateOne(
  { _id: "ACC-1" },
  { $inc: { balance: NumberDecimal("0.10") } }
);

// Type error: cannot $inc a non-numeric field
// { _id: 9, name: "Alice" }
db.users.updateOne({ _id: 9 }, { $inc: { name: 1 } });
// MongoServerError: Cannot apply $inc to a value of non-numeric type

$mul

Multiplies a numeric field by a given factor. The most important behavioral difference from $inc: if the field is missing, $mul is a silent no-op — it does not create the field, and it does not error.

  • Key difference from $inc: missing field is a silent no-op, not a field creation event
  • Factor of 0 sets the field to 0
  • Factor of 1 is effectively a no-op — value unchanged
  • Errors on non-numeric fields (same as $inc)
DANGER $mul on a missing field produces no error and no field creation. If your application assumes that a document always has the field after a $mul, that assumption is wrong. Either verify the field exists in a prior step, or use $set with a conditional to initialize it.
// Apply a 20% discount to all electronics (bulk price change)
db.products.updateMany(
  { category: "electronics" },
  { $mul: { price: 0.8 } }
  // price * 0.8 = 20% off
  // Products with NO price field are silently skipped — no error
);

// Factor behavior on { _id: 1, price: 100 }
db.products.updateOne({ _id: 1 }, { $mul: { price: 0 } });
// { price: 0 }  — factor 0 zeros out the field

db.products.updateOne({ _id: 1 }, { $mul: { price: 1 } });
// { price: 100 }  — factor 1 is a no-op

db.products.updateOne({ _id: 1 }, { $mul: { price: 1.5 } });
// { price: 150 }  — 50% price increase

db.products.updateOne({ _id: 1 }, { $mul: { price: 2.5 } });
// { price: 250 }  — 150% increase

// Missing field: SILENT no-op (unlike $inc which creates the field)
// Before: { _id: 4, name: "Widget" }  — no price field
db.products.updateOne({ _id: 4 }, { $mul: { price: 1.5 } });
// After: { _id: 4, name: "Widget" }  — price field still absent, no error

// Contrast: $inc creates the field
db.products.updateOne({ _id: 4 }, { $inc: { stock: 0 } });
// After: { _id: 4, name: "Widget", stock: 0 }  — $inc initializes at delta value

// Combine with $set to initialize then multiply in subsequent calls
db.products.updateOne(
  { _id: 4, price: { $exists: false } },   // only if price is missing
  { $set: { price: 100 } }                  // initialize it
);
04
$rename
Rename fields in-place — the schema migration tool
Schema

$rename moves a field to a new key name, preserving its value. It is the canonical operator for live schema migrations — renaming fields across an entire collection in one command without reading documents into application memory.

  • Renames the field and preserves its value and BSON type
  • Silent no-op if the source field does not exist — no error
  • If the target field already exists, it is overwritten (old target value lost)
  • Cannot rename _id
  • Hard MongoDB limitation: cannot rename fields inside array elements — the path must not traverse an array
  • Supports dot notation for renaming fields in nested subdocuments (as long as no array is in the path)
DANGER $rename cannot operate on paths that pass through an array. Attempting to rename "items.name" where items is an array will throw "The source field cannot be an array element, ...". For renaming fields inside array elements, use an aggregation pipeline update with $set + $map.
TIP Schema migration pattern: db.users.updateMany({}, { $rename: { "usr_email": "email" } }) migrates every document in the collection in one call. Documents that already use the new field name are untouched (no-op), so the operation is safe to run multiple times.
// Before: { _id: 1, usr_email: "alice@example.com", usr_name: "Alice", usr_age: 29 }

// Bulk schema migration — rename fields across entire collection
db.users.updateMany(
  {},
  { $rename: { "usr_email": "email", "usr_name": "name", "usr_age": "age" } }
);
// After: { _id: 1, email: "alice@example.com", name: "Alice", age: 29 }

// Renaming a nested field — dot notation is supported
// Before: { _id: 2, profile: { usr_bio: "Developer", avatar: "img.png" } }
db.users.updateOne(
  { _id: 2 },
  { $rename: { "profile.usr_bio": "profile.bio" } }
);
// After: { _id: 2, profile: { bio: "Developer", avatar: "img.png" } }
// avatar is untouched — only bio was renamed

// Source field does not exist — silent no-op
db.users.updateOne({ _id: 1 }, { $rename: { "nonExistent": "newName" } });
// Document unchanged, no error thrown — safe to run

// Target already exists — target is OVERWRITTEN (original target value lost)
// Before: { _id: 3, oldScore: 85, newScore: 90 }
db.users.updateOne(
  { _id: 3 },
  { $rename: { "oldScore": "newScore" } }
);
// After: { _id: 3, newScore: 85 }  — newScore replaced by oldScore's value (90 lost)

// ERROR: path passes through an array
// { _id: 4, items: [{ name: "Widget A" }, { name: "Widget B" }] }
db.docs.updateOne(
  { _id: 4 },
  { $rename: { "items.name": "items.title" } }
);
// MongoServerError: The source field cannot be an array element, ...

// Solution: aggregation pipeline update for arrays
db.docs.updateOne(
  { _id: 4 },
  [{
    $set: {
      items: {
        $map: {
          input: "$items",
          as:    "item",
          in:    { $mergeObjects: ["$$item", { title: "$$item.name" }] }  // copy name → title
        }
      }
    }
  },
  { $unset: "items.name" }]   // then remove the old key from each element
);
05
$min / $max / $currentDate
Conditional numeric updates and server-side timestamps
Conditional

$min

Updates the field only if the provided value is lower than the current value. If the field does not exist, it is created and set to the provided value. Eliminates read-then-write patterns for tracking minimums.

  • Writes only when new value < current value
  • Creates field if missing (set to provided value)
  • Works with dates: an earlier date qualifies as the minimum
  • String comparison is lexicographic"9" is greater than "200" because character '9' > '2'

$max

Updates the field only if the provided value is higher than the current value. Same rules as $min but with the comparison inverted. Ideal for high-score trackers and peak metric recording.

  • Writes only when new value > current value
  • Creates field if missing
  • Same lexicographic string comparison rules as $min
Operator Condition to write Use case examples
$min new value < current value Lowest price ever seen, best (fastest) response time, earliest event date
$max new value > current value High score tracker, peak concurrent users, most recent timestamp

$currentDate

Sets a field to the current server date/time. Two BSON types can be requested, with very different intended audiences:

  • true or { $type: "date" } → BSON ISODate — use this for all application timestamps
  • { $type: "timestamp" } → BSON Timestamp — reserved for MongoDB's internal replication and oplog; do not use for application logic
  • Uses server time, not client time — for client-event timestamps pass an explicit value via $set
  • Creates field if missing
WARN BSON Timestamp ({ $type: "timestamp" }) is an internal MongoDB type used by the oplog and replication system. It has special auto-increment semantics on the primary. Using it for application timestamps produces confusing results. Always use { $type: "date" } or simply true.
// $min — track lowest price a product has ever been sold at
// Before: { _id: "P1", title: "NVMe SSD", lowestPrice: 80 }

db.products.updateOne(
  { _id: "P1" },
  { $min: { lowestPrice: 65 } }
);
// 65 < 80  →  WRITE  →  { lowestPrice: 65 }

db.products.updateOne(
  { _id: "P1" },
  { $min: { lowestPrice: 100 } }
);
// 100 > 65  →  NO WRITE  →  { lowestPrice: 65 }  (unchanged)

// $min with dates — earlier date wins
db.events.updateOne(
  { eventId: "EVT-1" },
  { $min: { firstSeen: new Date("2024-03-01") } }
);
// Sets firstSeen only if 2024-03-01 is earlier than current firstSeen value

// $max — high-score tracker
// Before: { _id: "U1", name: "Alice", highScore: 1200 }

db.players.updateOne(
  { _id: "U1" },
  { $max: { highScore: 1350 } }
);
// 1350 > 1200  →  WRITE  →  { highScore: 1350 }

db.players.updateOne(
  { _id: "U1" },
  { $max: { highScore: 900 } }
);
// 900 < 1350  →  NO WRITE  →  { highScore: 1350 }  (unchanged)

// $max tracking peak concurrent connections
db.metrics.updateOne(
  { service: "api-gateway" },
  {
    $max:         { peakConnections: currentConnections },
    $currentDate: { lastChecked: true }
  }
);

// $currentDate — server-side timestamps
db.users.updateOne(
  { _id: "U1" },
  {
    $set: { status: "active" },
    $currentDate: {
      lastModified:   true,                        // ISODate ← use for app logic
      internalOpTime: { $type: "timestamp" }        // BSON Timestamp ← internal only
    }
  }
);

// Client event time vs server write time — pass explicitly for client time
db.clickEvents.insertOne({
  userId:      "U1",
  action:      "button_click",
  clientTime:  new Date(),        // time on the user's device
  serverTime:  null               // to be set by the server write
});
db.clickEvents.updateOne(
  { userId: "U1", serverTime: null },
  { $currentDate: { serverTime: true } }
);
06
Edge Cases
Gotchas, limits, and non-obvious behavior
Gotchas

$inc on a NaN field

Incrementing a field whose current value is NaN produces NaN. IEEE 754 dictates that NaN + n = NaN regardless of n. The field stays NaN and no error is raised.

// { _id: 1, score: NaN }
db.docs.updateOne({ _id: 1 }, { $inc: { score: 5 } });
// { _id: 1, score: NaN }  — NaN + 5 = NaN, no error, field unchanged

$mul on missing field is a SILENT no-op

Unlike $inc which creates the field at the delta value when missing, $mul silently does nothing. This asymmetry trips up developers expecting consistent behavior.

// { _id: 2, name: "Widget" }  — no price field
db.products.updateOne({ _id: 2 }, { $mul: { price: 2 } });
// { _id: 2, name: "Widget" }  — price still absent, no error, no trace

// $inc creates the field at the given value (0 + delta):
db.products.updateOne({ _id: 2 }, { $inc: { views: 0 } });
// { _id: 2, name: "Widget", views: 0 }  — field created

$unset on array index sets null — does not remove

// { _id: 3, tags: ["alpha", "beta", "gamma"] }
db.docs.updateOne({ _id: 3 }, { $unset: { "tags.1": "" } });
// { tags: ["alpha", null, "gamma"] }  — null hole, NOT removal

// Remove the null hole with $pull
db.docs.updateOne({ _id: 3 }, { $pull: { tags: null } });
// { tags: ["alpha", "gamma"] }  — compacted

Same field in two operators — conflict error

// INVALID — age appears in both $set and $inc
db.users.updateOne(
  { _id: 1 },
  {
    $set: { age: 30 },
    $inc: { age: 1 }   // conflict!
  }
);
// MongoServerError: Updating the path 'age' would create a conflict

// Fix: use separate update calls
db.users.updateOne({ _id: 1 }, { $set:  { age: 30 } });
db.users.updateOne({ _id: 1 }, { $inc:  { age: 1  } });

$rename cannot target paths inside arrays

// { _id: 4, items: [{ oldKey: "v1" }, { oldKey: "v2" }] }
db.docs.updateOne(
  { _id: 4 },
  { $rename: { "items.oldKey": "items.newKey" } }
);
// MongoServerError: The source field cannot be an array element, ...

// Workaround: aggregation pipeline update
db.docs.updateOne({ _id: 4 }, [
  { $set: { items: { $map: { input: "$items", as: "i",
      in: { newKey: "$$i.oldKey" } } } } }
]);

$set with dot notation vs object literal — reprise

// { _id: 5, address: { city: "Delhi", zip: "110001", country: "IN" } }

// Safe — only city is changed
db.docs.updateOne({ _id: 5 }, { $set: { "address.city": "Mumbai" } });
// { address: { city: "Mumbai", zip: "110001", country: "IN" } }  ✓

// Destructive — entire address subdocument replaced
db.docs.updateOne({ _id: 5 }, { $set: { address: { city: "Mumbai" } } });
// { address: { city: "Mumbai" } }  — zip and country DELETED

$min / $max string comparison is lexicographic — not numeric

DANGER "9" > "200" evaluates to true lexicographically because the character '9' (Unicode 0x39) has a higher code point than '2' (0x32). Never store numbers as strings if you plan to use $min/$max on them.
// { _id: 6, code: "200" }  — stored as a STRING
db.docs.updateOne({ _id: 6 }, { $max: { code: "9" } });
// "9" > "200" lexicographically → update fires → { code: "9" }
// WRONG — numerically 9 < 200, so this update should NOT have fired

// Fix: store as a number
// { _id: 6, code: 200 }  — stored as an INT
db.docs.updateOne({ _id: 6 }, { $max: { code: 9 } });
// 9 < 200 numerically → no update → { code: 200 }  ✓

$currentDate server time vs client time

NOTE $currentDate captures the moment of the write on the MongoDB server. The server time and client time can differ due to network latency, processing time, and clock drift. For recording when an event happened (e.g., user clicked a button), pass an explicit timestamp from application code: { $set: { eventTime: new Date() } }. Use $currentDate only for tracking when a document was written.