Updating & deleting documents

This section documents the current MongoDB 8.x server line as published at the MongoDB Server Manual, which is the reference these pages are written and verified against. No specific patch version is pinned. Some capabilities (Atlas Search, Atlas Vector Search, and parts of encryption and backup) are Atlas-only — they are linked, not documented in depth.

This content was generated with the assistance of AI and should be verified against the official manual before being relied on in production, since MongoDB iterates quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

Writes that change existing data come in two shapes: operator updates, which modify named fields in place, and full replacement, which swaps the whole document (except _id). This page covers the update and delete methods, the field and array update operators, upserts, the findOneAndX family, and pipeline-form updates. The manual’s overview is Update Documents.

updateOne, updateMany, replaceOne

updateOne(filter, update) modifies the first document matching filter; updateMany(filter, update) modifies all of them. The filter is exactly the filter document from Reading data with the Query API. An operator update uses $-prefixed operators and touches only the named fields; a replacement passes a plain document and replaces every field but _id. replaceOne is the dedicated method for the replacement case.

// operator update: only "status" and "updatedAt" change
db.orders.updateOne(
  { _id: 42 },
  { $set: { status: "shipped", updatedAt: new Date() } }
)

// updateMany: same change across many documents
db.orders.updateMany({ status: "open" }, { $set: { status: "cancelled" } })

// replaceOne: whole document swapped (keeps _id)
db.orders.replaceOne({ _id: 42 }, { customerId: 7, status: "shipped", lines: [] })

The methods return a write result describing what happened:

{
  "acknowledged": true,
  "matchedCount": 1,
  "modifiedCount": 1,
  "upsertedId": null
}

matchedCount counts documents that matched the filter; modifiedCount counts those actually changed (a $set to the value a field already holds matches but does not modify).

Field update operators

The full list is Update Operators.

db.products.updateOne(
  { _id: "A-1" },
  {
    $set:         { name: "Widget", "spec.color": "red" },  // set fields (creates if absent)
    $unset:       { legacyCode: "" },                       // remove a field
    $rename:      { desc: "description" },                   // rename a field
    $inc:         { views: 1, stock: -2 },                  // add to numeric fields
    $mul:         { price: 1.1 },                           // multiply a numeric field
    $min:         { lowestSeen: 9.99 },                     // set only if given value is lower
    $max:         { highestSeen: 19.99 },                   // set only if given value is higher
    $currentDate: { updatedAt: true },                      // set to current date/time
    $setOnInsert: { createdAt: new Date() }                 // applied only on an upsert insert
  }
)

$setOnInsert has no effect on a plain update; it only seeds fields when an upsert creates a new document (see Upserts).

Array update operators

$push (with $each, $slice, $sort, $position)

// append one element
db.carts.updateOne({ _id: 1 }, { $push: { items: "sku-9" } })

// append several, keep the array sorted and capped at 10, inserting at index 0
db.feeds.updateOne(
  { _id: 1 },
  { $push: { entries: {
      $each: [ { ts: 3 }, { ts: 1 } ],
      $sort: { ts: 1 },
      $slice: -10,
      $position: 0
  } } }
)

$addToSet

// add only if not already present (set semantics)
db.products.updateOne({ _id: "A-1" }, { $addToSet: { tags: { $each: ["sale", "new"] } } })

$pop

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

$pull and $pullAll

// $pull: remove every element matching a condition
db.carts.updateOne({ _id: 1 }, { $pull: { items: { qty: { $lte: 0 } } } })

// $pullAll: remove every listed value exactly
db.carts.updateOne({ _id: 1 }, { $pullAll: { skus: ["sku-1", "sku-2"] } })

Positional operators $, $[], $[<identifier>]

$ updates the first array element matched by the query filter. $[] updates every element. $[<identifier>] updates every element matched by a matching entry in arrayFilters. These are documented at $, https://www.mongodb.com/docs/manual/reference/operator/update/positional-all/], and $[<identifier>`].

// $ : bump the qty of the matched line only
db.orders.updateOne(
  { _id: 1, "lines.sku": "A-1" },
  { $set: { "lines.$.qty": 5 } }
)

// $[] : apply a discount to every line
db.orders.updateOne({ _id: 1 }, { $mul: { "lines.$[].price": 0.9 } })

// $[elem] with arrayFilters : only lines over 100
db.orders.updateOne(
  { _id: 1 },
  { $set: { "lines.$[big].flagged": true } },
  { arrayFilters: [ { "big.price": { $gt: 100 } } ] }
)

Upserts

Passing { upsert: true } makes an update that matches nothing insert a new document instead. The new document is built from the equality conditions in the filter plus any $setOnInsert and other update operators.

db.counters.updateOne(
  { _id: "invoice" },                 // equality condition -> seeds _id
  {
    $inc: { seq: 1 },                 // applied to the new document too
    $setOnInsert: { createdAt: new Date() }
  },
  { upsert: true }
)

If no invoice counter exists, the upsert inserts { _id: "invoice", seq: 1, createdAt: <now> }; the write result then carries upsertedId. See the upsert notes.

findOneAndUpdate, findOneAndReplace, findOneAndDelete

These modify (or delete) a single document and return a document in one atomic step. returnDocument chooses which version comes back: "before" (the default) or "after". They also accept sort (to pick which document when several match) and projection.

// atomically claim the next queued job and get it back
const job = db.jobs.findOneAndUpdate(
  { status: "queued" },
  { $set: { status: "running", startedAt: new Date() } },
  { sort: { priority: -1 }, returnDocument: "after" }
)

db.people.findOneAndReplace({ _id: 1 }, { name: "Ada", role: "admin" }, { returnDocument: "after" })

db.jobs.findOneAndDelete({ status: "done" }, { sort: { finishedAt: 1 } })

Update with an aggregation pipeline

The update argument can be an array — an aggregation pipeline ($set/$addFields, $unset, $replaceRoot, $replaceWith). This allows an update that references other fields of the same document.

// total is computed from price and qty already in the document
db.orders.updateMany(
  {},
  [ { $set: { total: { $multiply: ["$price", "$qty"] },
              updatedAt: "$$NOW" } } ]
)

The allowed stages and worked examples are in Update Documents with Aggregation Pipeline; the expression language is the same one described in Aggregation pipeline.

deleteOne and deleteMany

deleteOne(filter) removes the first matching document; deleteMany(filter) removes all of them. The filter is again the query filter document. The walkthrough is Delete Documents.

db.sessions.deleteOne({ _id: "s-1" })

db.sessions.deleteMany({ expiresAt: { $lt: new Date() } })

// delete every document, keep the collection, its indexes, and its options
db.sessions.deleteMany({})

The delete result reports how many were removed:

{ "acknowledged": true, "deletedCount": 128 }

drop() versus deleting all documents

db.collection.deleteMany({}) empties a collection but leaves the collection, its indexes, and its configured options in place. db.collection.drop() removes the collection itself — including every index — and is typically much faster because it does not process documents one by one.

db.sessions.drop()   // collection and its indexes are gone