MongoDB Cheatsheet

Update Operators

Use this MongoDB reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Update methods

Every update is (filter, update, options). The filter is an ordinary query, so everything from the query operators page applies.

db.orders.updateOne({ _id: ObjectId("6612...") }, { $set: { status: "shipped" } })
db.orders.updateMany({ status: "pending" }, { $set: { status: "cancelled" } })
db.orders.replaceOne({ _id: 1 }, { sku: "A-1", qty: 4 })

db.orders.findOneAndUpdate(
  { status: "pending" },
  { $set: { status: "processing" }, $currentDate: { claimed_at: true } },
  { sort: { placed_at: 1 }, returnDocument: "after" }
)
MethodReturns
updateOneCounts. Touches at most one document, the first the filter finds
updateManyCounts. Not atomic across documents, other writers interleave
replaceOneCounts. Swaps the whole document, _id excepted
findOneAndUpdateThe document itself, before or after the change
findOneAndReplaceSame, with a replacement
bulkWriteMixed operations in one round trip
// { acknowledged: true, insertedId: null,
//   matchedCount: 1, modifiedCount: 1, upsertedCount: 0 }

matchedCount and modifiedCount differ when the new value already equals the old one, because the server skips a no-op write. A modifiedCount of 0 with matchedCount of 1 means "found it, nothing changed", not "failed". findOneAndUpdate defaults to returnDocument: "before", so pass "after" when you want the result of the change. The old shell spelled that returnNewDocument: true.

Field operators

OperatorDoes
$setSet a field, creating it and any missing parents
$unsetRemove a field. The value you pass is ignored, convention is ""
$incAdd a number, negative to subtract. Creates the field if absent
$mulMultiply. Creates the field as 0 if absent
$minSet only if the given value is less than the current one
$maxSet only if greater
$renameRename a field. Cannot move a field into or out of an array
$currentDateServer clock. true for a Date, { $type: "timestamp" } for a Timestamp
$setOnInsertApply only when an upsert actually inserts
$bitand / or / xor on an integer field
db.products.updateOne({ sku: "A-1" }, {
  $set:   { "meta.reviewed": true, price: NumberDecimal("64.99") },
  $inc:   { views: 1, stock: -2 },
  $min:   { lowest_price: NumberDecimal("64.99") },
  $unset: { legacy_code: "" },
  $rename: { descr: "description" },
  $currentDate: { updated_at: true }
})

$set with a dotted path creates the intermediate subdocuments, so { $set: { "a.b.c": 1 } } on an empty document produces { a: { b: { c: 1 } } }. That convenience is also how typos silently create a second, misspelled field.

Upsert and $setOnInsert

upsert: true inserts when the filter matches nothing. The new document is built from the equality clauses of the filter plus the update operators.

db.counters.updateOne(
  { _id: "orders", period: "2026-01" },
  { $inc: { seq: 1 }, $setOnInsert: { created_at: new Date() } },
  { upsert: true }
)
// { matchedCount: 0, modifiedCount: 0, upsertedCount: 1,
//   upsertedId: ... } on the first call
RuleDetail
Equality filters seed the document{ _id: "orders", period: "2026-01" } is copied in
Non-equality filters do not{ qty: { $gt: 5 } } contributes nothing to the insert
$setOnInsert runs on insert onlyPerfect for created_at
$set runs on both pathsSo updated_at belongs in $set
Concurrent upserts can collideTwo racing upserts can raise a duplicate key error. Put a unique index on the filter fields and retry once

Array operators

OperatorDoes
$pushAppend. Duplicates allowed
$push + $eachAppend several values
$push + $each + $sliceAppend then trim. Negative keeps the last n, positive the first n, 0 empties
$push + $each + $sortSort the array after appending. $sort: { score: -1 } for documents, $sort: -1 for scalars
$push + $each + $positionInsert at an index instead of the end
$addToSetAppend only if not already present. $each adds several
$pop1 removes the last element, -1 the first
$pullRemove every element matching a query condition
$pullAllRemove every element exactly equal to one of these values
// A capped, sorted leaderboard in one write
db.players.updateOne({ _id: 1 }, {
  $push: {
    scores: {
      $each: [ { v: 88, at: new Date() } ],
      $sort: { v: -1 },
      $slice: 10
    }
  }
})

db.posts.updateOne({ _id: 1 }, { $addToSet: { tags: { $each: ["mongodb", "mql"] } } })
db.posts.updateOne({ _id: 1 }, { $pull: { comments: { spam: true } } })
db.posts.updateOne({ _id: 1 }, { $pull: { scores: { $lt: 50 } } })
db.posts.updateOne({ _id: 1 }, { $pullAll: { tags: ["old", "draft"] } })
db.posts.updateOne({ _id: 1 }, { $pop: { events: -1 } })

$slice and $sort are only legal alongside $each, even when you push one value. Modifiers apply in a fixed order: $each, then $sort, then $slice. $addToSet compares whole values, so two subdocuments differing only in field order count as different elements.

Positional operators

Three ways to say which element. Pick the wrong one and you update everything or nothing.

OperatorUpdatesNeeds
$The first element matched by the queryThe array field must appear in the filter
$[]Every elementNothing
$[<id>]Every element matching an arrayFilters entryarrayFilters (3.6+)
// First matching element only
db.orders.updateOne(
  { _id: 1, "items.sku": "A-1" },
  { $set: { "items.$.qty": 5 } }
)

// Every element
db.orders.updateOne({ _id: 1 }, { $inc: { "items.$[].qty": 1 } })

// Every element that matches a condition
db.orders.updateOne(
  { _id: 1 },
  { $set: { "items.$[low].backorder": true } },
  { arrayFilters: [ { "low.qty": { $lt: 2 } } ] }
)

// Nested arrays, one identifier per level
db.orders.updateOne(
  { _id: 1 },
  { $set: { "items.$[i].tags.$[t].hot": true } },
  { arrayFilters: [ { "i.sku": "A-1" }, { "t": "sale" } ] }
)

$ updates one element and there is no "update all matching" form of it, which is why arrayFilters exists. An identifier must start with a lowercase letter and contain only alphanumerics, and every identifier you use must appear in arrayFilters or the server rejects the update. $ also cannot be used on the insert half of an upsert.

Update with an aggregation pipeline

Since 4.2 the update can be an array of stages, which is the only way to compute a field from other fields of the same document without reading it first.

db.orders.updateMany({}, [
  { $set: {
      total: { $sum: "$items.subtotal" },
      item_count: { $size: "$items" },
      slug: { $toLower: { $concat: ["$brand", "-", "$sku"] } }
  } },
  { $set: { needs_review: { $gt: ["$total", 1000] } } },
  { $unset: "legacy_total" }
])

Only these stages are allowed: $addFields / $set, $project / $unset, $replaceRoot / $replaceWith. Field paths use "$field" on the right-hand side, which is exactly what plain $set cannot do.

Replacement is not an update

A plain document without operators is a replacement, and modern drivers refuse it in updateOne rather than doing it silently.

db.orders.updateOne({ _id: 1 }, { status: "paid" })
// Error: update document requires atomic operators

db.orders.replaceOne({ _id: 1 }, { status: "paid" })
// Document is now exactly { _id: 1, status: "paid" }. Everything else is gone.

_id is immutable, so it survives a replacement and cannot be changed by one. The retired db.collection.update() helper accepted either shape, which is how whole documents used to get wiped by accident. Use updateOne, updateMany, or replaceOne and the ambiguity disappears.