deleteOne and deleteMany
Deleting documents in MongoDB means permanently removing them from a collection based on a filter you supply. There is no soft-delete, trash bin, or built-in undo — once deleteOne() or deleteMany() finishes, those documents are gone. MongoDB gives you two purpose-built methods for this job: deleteOne() removes at most a single matching document, and deleteMany() removes every document that matches the filter. Picking the right one, and getting the filter right, matters a lot — an overly broad or accidentally empty filter passed to deleteMany() can wipe out an entire collection in one call.
Overview / How it works
Both methods take a filter document with exactly the same syntax you already use with find(): field-to-value matches, or operator documents like { status: { $in: ["cancelled", "refunded"] } }. MongoDB’s query planner evaluates that filter the same way it does for a read — if a usable index exists, it performs an IXSCAN to locate matching documents directly; if not, it falls back to a COLLSCAN, walking every document in the collection to test the filter. This is why delete performance on a large collection depends on indexing exactly the way query performance does.
deleteOne() stops as soon as it finds and removes the first matching document. Which document that is depends on the order MongoDB happens to encounter them in (typically index or storage order) — it is not guaranteed to be the "first" by any field unless your filter uniquely identifies one document (like matching on _id). deleteMany() keeps scanning and removes every document that matches, one at a time.
Each individual document removal is atomic — a document is either fully deleted or untouched, there is no partial state. But deleteMany() is not one single atomic multi-document operation: if it is removing 50,000 documents and the server crashes halfway through, some will already be gone and others will not. On a replica set, every deleted document generates its own entry in the oplog, so a deleteMany() that removes 10,000 documents produces 10,000 individual delete events, not one — this is also what a change stream listening on the collection will observe: one delete event per document, in sequence.
Both methods return a write result object, not the deleted document itself: { acknowledged: true, deletedCount: N }. If you need the actual content of the document you just removed (for logging, auditing, or returning it to a caller), use findOneAndDelete() instead, which atomically finds, deletes, and returns that one document.
Syntax
db.collection.deleteOne(filter, options);
db.collection.deleteMany(filter, options);
| Parameter | Description |
|---|---|
filter |
A query document selecting which document(s) to remove. Uses the same syntax as find(). An empty document {} matches every document in the collection. |
options.writeConcern |
Overrides the default write concern, e.g. { w: "majority" }, to control how many replica set members must acknowledge the delete. |
options.collation |
Language-specific string comparison rules (case sensitivity, accent handling) to apply when matching string fields in the filter. |
options.hint |
Forces MongoDB to use a specific index for the filter, bypassing the query planner’s own choice. Useful when you know a better index than the planner picks. |
Examples
Example 1: removing a single order by its unique _id — the safest and most predictable use of deleteOne().
db.orders.deleteOne({ _id: ObjectId("64fa1f3b2c9e4a1d88f0a1b2") });
{ acknowledged: true, deletedCount: 1 }
Because _id is unique and indexed by default, MongoDB uses an IXSCAN to jump straight to that document and remove it. deletedCount: 1 confirms exactly one document was removed; if no document had that _id, the call would still succeed and simply return deletedCount: 0 rather than throwing an error.
Example 2: cleaning up old cancelled orders with deleteMany() and a compound filter.
db.orders.deleteMany({
status: "cancelled",
createdAt: { $lt: new Date("2026-01-01") }
});
{ acknowledged: true, deletedCount: 47 }
Every document where status equals "cancelled" AND createdAt is before January 1, 2026 is removed. If { status: 1, createdAt: 1 } exists as a compound index, MongoDB can use it directly (equality field first, range field second — the ESR rule) instead of scanning the whole collection.
Example 3: the danger of deleteOne() on a non-unique filter.
db.reviews.deleteOne({ flagged: true });
{ acknowledged: true, deletedCount: 1 }
Suppose 12 reviews are currently flagged. This call still reports success and deletedCount: 1, but it only removed one arbitrary flagged review — the other 11 are untouched. That’s correct behavior for deleteOne(), but it’s a common source of confusion when someone expected all flagged reviews to disappear. If the goal was "remove every flagged review," the fix is deleteMany({ flagged: true }) instead.
How it works step by step
- MongoDB parses the filter and asks the query planner to choose an access path — an index scan (
IXSCAN) if a suitable index exists and is selected, otherwise a full collection scan (COLLSCAN). You can check which one is used withdb.orders.explain().deleteMany({ status: "cancelled" }). - MongoDB walks the chosen access path, testing each candidate document against the full filter.
- For
deleteOne(), the very first match found is removed and the operation stops immediately. FordeleteMany(), every match found is removed as the scan continues to the end. - Each removal deletes the document from the collection’s storage and removes its entries from every index defined on the collection — more indexes mean more index-entry removals per deleted document, which adds overhead.
- On a replica set, each individual document removal is written to the oplog as its own entry and replicated to secondaries; a
deleteMany()of 10,000 documents means 10,000 oplog entries, not one. - MongoDB returns the write result,
{ acknowledged, deletedCount }, once the write concern you specified (default{ w: 1 }, meaning just the primary) has been satisfied.
Common Mistakes
Mistake 1: using deleteOne() when you meant deleteMany().
// Wrong: only removes ONE cancelled order, the rest remain
db.orders.deleteOne({ status: "cancelled" });
db.orders.deleteMany({ status: "cancelled" });
Mistake 2: comparing _id to a plain string instead of an ObjectId.
// Wrong: idFromUrl is a string, _id is stored as an ObjectId, so nothing matches
const idFromUrl = "64fa1f3b2c9e4a1d88f0a1b2";
db.orders.deleteOne({ _id: idFromUrl });
// -> { acknowledged: true, deletedCount: 0 }
const idFromUrl = "64fa1f3b2c9e4a1d88f0a1b2";
db.orders.deleteOne({ _id: new ObjectId(idFromUrl) });
Mistake 3: an accidentally empty filter deletes the whole collection.
// Wrong: status is undefined, so MongoDB ignores that clause entirely,
// leaving an effectively empty filter that matches EVERY document
const status = undefined;
db.orders.deleteMany({ status: status });
// Always confirm the filter isn't accidentally empty before running it,
// e.g. by running the equivalent find() or countDocuments() first
db.orders.countDocuments({ status: "cancelled" });
db.orders.deleteMany({ status: "cancelled" });
Mistake 4: deleting on an unindexed field in a large collection.
// Wrong: no index on notes, so this triggers a full COLLSCAN across
// the whole collection, holding write activity up far longer than needed
db.orders.deleteMany({ notes: /refund requested/i });
// Narrow the scan with an indexed field first
db.orders.createIndex({ status: 1 });
db.orders.deleteMany({ status: "cancelled", notes: /refund requested/i });
Best Practices
- Before running a
deleteMany()you can’t undo, run the equivalentfind(filter)orcountDocuments(filter)first to confirm exactly which and how many documents will be affected. - Only reach for
deleteOne()when the filter uniquely identifies one document (ideally by_id); don’t rely on it matching just one document by coincidence. - Index the fields used in delete filters the same way you would for reads, and verify with
explain()that you’re getting anIXSCAN, not aCOLLSCAN. - To remove every document and empty a collection entirely, prefer
db.collection.drop()overdeleteMany({})— it’s far faster and you simply recreate any indexes afterward. - Set an explicit write concern, like
{ writeConcern: { w: "majority" } }, for deletes you cannot afford to lose acknowledgment of during a failover. - Consider a soft-delete flag (
deletedAtorstatus: "deleted") instead of a physical delete when you need to preserve history for auditing. - Wrap deletes that must succeed or fail together across multiple collections in a multi-document transaction; a single
deleteMany()call is not itself all-or-nothing across its whole batch.
Practice Exercises
1. In a products collection, delete every product where discontinued is true and stock is 0. Expected result shape: { acknowledged: true, deletedCount: <n> }.
2. Given a productId that arrives as a string from a URL parameter, write the correct deleteOne() call to remove that product by its _id. Hint: you’ll need to convert the string to the right type first.
3. You want to remove exactly one duplicate document from a sessions collection while leaving the rest of the duplicates alone. Explain why filtering on a shared field like userId is risky for this, and write a filter based on a unique field instead.
Summary
deleteOne()removes at most one matching document;deleteMany()removes every matching document.- Both accept a filter document with the same syntax as
find(), and both return{ acknowledged, deletedCount }rather than the deleted document itself. - Deletes are physically permanent with no built-in undo — verify the filter with
find()orcountDocuments()before running it. - Delete filters are planned like read queries: an unindexed filter causes a full collection scan, checkable with
explain(). - Each deleted document gets its own oplog entry;
deleteMany()is not one atomic multi-document operation, even though each individual document removal is atomic. - Comparing
_idto a plain string instead of anObjectIdis one of the most common causes of a delete silently matching nothing. - Use
drop()instead ofdeleteMany({})when the goal is to remove every document and empty the collection.
