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 }
| Method | Removes | Returns |
|---|---|---|
deleteOne(filter) | The first match | deletedCount |
deleteMany(filter) | Every match. Not atomic across documents | deletedCount |
findOneAndDelete(filter, opts) | One match, honouring sort | The 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.
| Aspect | deleteMany({}) | drop() |
|---|---|---|
| Documents | Removed one by one | Gone with the collection |
| Indexes | Kept, and updated for every delete | Dropped along with the collection |
| Validators, collation, options | Kept | Gone, recreate them |
| Cost | Proportional to document count | Effectively constant |
| Oplog | One entry per document | One entry |
| Change streams | A delete event per document | A drop event, then invalidate |
| Rollback | None, it is not a transaction | None |
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 key | Extra fields |
|---|---|
insertOne | document |
updateOne · updateMany | filter, update, and optionally upsert, arrayFilters, collation, hint |
replaceOne | filter, replacement, and optionally upsert, collation, hint |
deleteOne · deleteMany | filter, 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.
| Aspect | ordered: true (default) | ordered: false |
|---|---|---|
| Execution | Serially, in array order | Server may group and parallelize by type |
| On error | Stops, later operations never run | Continues, all errors reported at the end |
| Already-applied writes | Stay applied | Stay applied |
| Speed | Slower | Faster, 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
| Limit | Value | What happens past it |
|---|---|---|
| Writes per server batch | 100,000 (maxWriteBatchSize) | The driver splits your array into groups automatically |
| BSON document size | 16MB | An oversized document is rejected outright |
| Command message size | 48MB in practice | The driver splits again on total size |
| Aggregate array size | Your client's memory | Nothing 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 })