MongoDB Cheatsheet

Delete and Bulk Writes

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

Deleting documents

Three delete methods, and every one takes an ordinary query filter. There is no deleteAll, an empty filter is how you say "all".

db.orders.deleteOne({ _id: ObjectId("6612...") })
db.orders.deleteMany({ status: "cancelled" })
db.orders.deleteMany({})                    // every document, one at a time
db.orders.findOneAndDelete({ status: "pending" }, { sort: { placed_at: 1 } })
// { acknowledged: true, deletedCount: 12 }
MethodRemovesReturns
deleteOne(filter)The first matchdeletedCount
deleteMany(filter)Every match. Not atomic across documentsdeletedCount
findOneAndDelete(filter, opts)One match, honouring sortThe deleted document

An empty filter with deleteOne is legal and deletes an arbitrary single document, which is almost never what anyone means. A deleteMany on a large collection runs for a long time, writes one oplog entry per document, and can be interrupted part way through leaving the rest in place.

deleteMany({}) is not drop()

They both leave you with an empty collection and they cost wildly different things.

AspectdeleteMany({})drop()
DocumentsRemoved one by oneGone with the collection
IndexesKept, and updated for every deleteDropped along with the collection
Validators, collation, optionsKeptGone, recreate them
CostProportional to document countEffectively constant
OplogOne entry per documentOne entry
Change streamsA delete event per documentA drop event, then invalidate
RollbackNone, it is not a transactionNone
db.orders.deleteMany({})        // empty, indexes and validator intact
db.orders.drop()                // collection, indexes, and validator gone
db.dropDatabase()               // every collection in the current db
db.orders.drop({ writeConcern: { w: "majority" } })

Reach for deleteMany({}) when you want to keep the schema validator and the index set you carefully built. Reach for drop() when you are resetting a fixture. Note that drop() on a sharded collection also clears its routing metadata, so you have to call sh.shardCollection again afterwards.

bulkWrite

One round trip, many mixed operations. This is the right tool for a migration or an importer, and it is dramatically faster than a loop of updateOne calls.

db.orders.bulkWrite([
  { insertOne: { document: { sku: "C-3", qty: 1 } } },
  { updateOne: {
      filter: { sku: "A-1" },
      update: { $inc: { qty: 2 } },
      upsert: true
  } },
  { updateMany: {
      filter: { status: "pending", placed_at: { $lt: ISODate("2026-01-01") } },
      update: { $set: { status: "stale" } }
  } },
  { replaceOne: { filter: { sku: "B-2" }, replacement: { sku: "B-2", qty: 9 } } },
  { deleteOne: { filter: { sku: "OLD" } } },
  { deleteMany: { filter: { status: "cancelled" } } }
], { ordered: false })
Operation keyExtra fields
insertOnedocument
updateOne · updateManyfilter, update, and optionally upsert, arrayFilters, collation, hint
replaceOnefilter, replacement, and optionally upsert, collation, hint
deleteOne · deleteManyfilter, and optionally collation, hint

The result rolls up every operation into one document:

// {
//   acknowledged: true,
//   insertedCount: 1,  matchedCount: 4,  modifiedCount: 4,
//   deletedCount: 7,   upsertedCount: 1,
//   insertedIds: { '0': ObjectId("...") },
//   upsertedIds: { '1': ObjectId("...") }
// }

insertedIds and upsertedIds are keyed by the operation's index in your array, which is how you map a generated id back to the input that produced it.

Ordered semantics

ordered means the same thing here as on insertMany, and it also controls whether the server may reorder work.

Aspectordered: true (default)ordered: false
ExecutionSerially, in array orderServer may group and parallelize by type
On errorStops, later operations never runContinues, all errors reported at the end
Already-applied writesStay appliedStay applied
SpeedSlowerFaster, often much
try {
  db.users.bulkWrite(ops, { ordered: false })
} catch (e) {
  print(e.result.nInserted)
  e.writeErrors.forEach(w => print(w.index, w.code, w.errmsg))
}

A bulk write is not a transaction in either mode. If you need all-or-nothing, wrap the call in session.withTransaction on a replica set, which caps you at the transaction limits instead.

Batch limits

LimitValueWhat happens past it
Writes per server batch100,000 (maxWriteBatchSize)The driver splits your array into groups automatically
BSON document size16MBAn oversized document is rejected outright
Command message size48MB in practiceThe driver splits again on total size
Aggregate array sizeYour client's memoryNothing server side stops you materializing 10 million ops

Drivers auto-split, so passing 500,000 operations works and quietly becomes five batches. The reason to chunk manually at 10,000 to 50,000 is client memory and the ability to log progress, not a server limit. Note that a split batch means the split points are not atomic even with ordered: true.

// Manual chunking, driven by a cursor so nothing large sits in memory
let ops = [];
db.staging.find().forEach(doc => {
  ops.push({ updateOne: {
    filter: { _id: doc._id }, update: { $set: { migrated: true } }, upsert: true
  } });
  if (ops.length === 10000) { db.orders.bulkWrite(ops, { ordered: false }); ops = []; }
});
if (ops.length) db.orders.bulkWrite(ops, { ordered: false });

MongoDB 8.0 adds a cluster-level bulkWrite that spans databases and collections, exposed in mongosh as Mongo.bulkWrite(). Note the different shape, a namespace plus a name per operation:

db.getMongo().bulkWrite([
  { namespace: "shop.orders", name: "insertOne", document: { sku: "C-3" } },
  { namespace: "audit.events", name: "insertOne", document: { kind: "order" } }
], { ordered: false, verboseResults: true })