MongoDB deleteMany: The Empty Filter That Deletes Everything

MongoDB deleteMany removes every document that matches a filter, and it never asks “are you sure?”. In SQL, a DELETE without a WHERE clause at least looks dangerous. In MongoDB, the dangerous version is a pair of empty curly braces.

Gouache painting of an empty pantry shelf with dust outlines of missing jars, a broom, and one vermilion jar left on the floor.

A Small Collection to Test On

Let’s test every delete method on a small collection and look at the traps. I ran everything below on MongoDB 9.0.2 with mongosh 2.13.0. If you don’t have MongoDB yet, my post on how to install MongoDB on Windows walks through it. The collection holds eight orders. Look at the last one: Hugo’s order has no status field at all. That detail matters later.

use deleteDemo
db.orders.insertMany([
  { _id: 1, customer: 'Asha',  status: 'shipped',   total: 120, orderDate: ISODate('2026-09-01') },
  { _id: 2, customer: 'Ben',   status: 'cancelled', total: 45,  orderDate: ISODate('2026-09-02') },
  { _id: 3, customer: 'Chen',  status: 'pending',   total: 300, orderDate: ISODate('2026-09-03') },
  { _id: 4, customer: 'Dara',  status: 'cancelled', total: 80,  orderDate: ISODate('2026-09-04') },
  { _id: 5, customer: 'Eli',   status: 'shipped',   total: 60,  orderDate: ISODate('2026-09-05') },
  { _id: 6, customer: 'Farah', status: 'cancelled', total: 25,  orderDate: ISODate('2026-09-06') },
  { _id: 7, customer: 'Gita',  status: 'pending',   total: 150, orderDate: ISODate('2026-09-07') },
  { _id: 8, customer: 'Hugo',                       total: 90,  orderDate: ISODate('2026-09-08') }
])

Count First, Then Delete

Before any delete, run countDocuments with the exact same filter. It costs nothing and tells you what’s about to disappear. Here, three orders are cancelled.

db.orders.countDocuments({ status: 'cancelled' })
// 3

deleteOne removes the first matching document it finds and stops. deleteMany removes all the matches. Both return an object with deletedCount, which is the number to check afterward.

db.orders.deleteOne({ status: 'cancelled' })
// { acknowledged: true, deletedCount: 1 }
db.orders.deleteMany({ status: 'cancelled' })
// { acknowledged: true, deletedCount: 2 }

The counts add up: one plus two is the three we counted. When the deletedCount doesn’t match your count, stop and find out why before you run anything else.

One more detail about deleteOne. It removes the first document that matches, and you don’t control which one that is. Here it took Ben’s order, the first one inserted, but that isn’t a promise. When you mean one specific document, filter on its _id. That turns “some cancelled order” into “this cancelled order”.

If you want to see the document you removed, use findOneAndDelete. It deletes one document and returns it. With a sort, you control which one goes first. Here it removes the oldest pending order.

db.orders.findOneAndDelete({ status: 'pending' }, { sort: { orderDate: 1 } })
// { _id: 3, customer: 'Chen', status: 'pending', total: 300,
//   orderDate: ISODate('2026-09-03T00:00:00.000Z') }

Trap One: A Typo Deletes Nothing

MongoDB doesn’t check field names against a schema. If you misspell a field, the filter is still valid. It matches no documents, and the delete reports zero.

db.orders.deleteMany({ stauts: 'shipped' })
// { acknowledged: true, deletedCount: 0 }

That one is harmless, but it fools scripts. A cleanup job that deletes zero rows every night looks healthy in the log. Nobody notices until the collection is huge. A job that expects deletes should treat a zero as a warning.

Trap Two: A Variable That Was Never Set

This one is easy to miss. Scripts build filters from variables. If the variable was never set, the filter gets the value undefined. The shell sends undefined as null. In MongoDB, a null filter matches documents where the field is null or missing.

let statusToRemove;
db.orders.countDocuments({ status: statusToRemove })
// 1
db.orders.find({ status: statusToRemove }, { _id: 1, customer: 1 })
// [ { _id: 8, customer: 'Hugo' } ]

Hugo’s order has no status, so it matches. Run deleteMany with that filter and a real order disappears. In a large collection with older documents that lack a field, that could be thousands of orders. The fix is a guard in the script that refuses to run when a filter value is missing.

if (statusToRemove === undefined || statusToRemove === null) {
  throw new Error('statusToRemove is not set, nothing deleted');
}
db.orders.deleteMany({ status: statusToRemove })

The Empty Filter

Now the one from the title. An empty filter, written as two curly braces, matches every document. To test it safely, I copied the four remaining orders into a collection called archive. I also added an index on customer.

db.archive.insertMany(db.orders.find().toArray())
db.archive.createIndex({ customer: 1 })
db.archive.deleteMany({})
// { acknowledged: true, deletedCount: 4 }
db.archive.getIndexes().map(i => i.name)
// [ '_id_', 'customer_1' ]

Every document is gone, but the collection and its indexes stay. That’s the MongoDB version of DELETE without a WHERE clause. If you want the whole collection gone, drop is the honest command. It removes the collection, its documents and its indexes in one step.

db.archive.drop()
// true
db.getCollectionNames()
// [ 'orders' ]

You could say the empty filter is a feature, not a trap. Fair point. Clearing a staging collection is a normal job. deleteMany({}) does it and keeps the indexes you’ll need for the next load. The danger isn’t the command. The danger is running it against the wrong collection or the wrong database, with no count first. On a big collection, it’s also slow, because it deletes documents one at a time. When you mean “empty the whole thing”, drop and recreate is the faster path.

The Same Ideas in SQL

MongoDBSQL Server
deleteOne({ status: ‘cancelled’ })DELETE TOP (1) FROM Orders WHERE Status = ‘cancelled’
deleteMany({ status: ‘cancelled’ })DELETE FROM Orders WHERE Status = ‘cancelled’
deleteMany({})DELETE FROM Orders
drop()DROP TABLE Orders
{ status: null }WHERE Status IS NULL (plus rows where the column doesn’t exist, which SQL can’t have)

The last row is the real difference. A SQL table has fixed columns, so a missing column is impossible. A MongoDB collection can hold documents with different fields, so “missing” is a separate case you have to think about. If you come from SQL Server, my post on SQL terms vs MongoDB terms maps the rest of the vocabulary.

If you think in SQL, use that as a check. Before you run a deleteMany, say the SQL out loud. For example: DELETE FROM orders WHERE status is cancelled. If the filter you typed doesn’t match that sentence, fix it first. An empty pair of braces says “DELETE FROM orders” with nothing after it. Nobody runs that by accident in SQL Server.

A Safe Delete Routine

A safe routine for any delete outside a test database is short. Count with the filter. Run find with the same filter and read a few documents. Check that every variable in the filter has a value. Then delete and compare deletedCount with the count. For data you could need again, copy the matches to an archive collection first. Or mark them with a status instead of removing them.

MongoDB has no undo. Each single document delete is atomic, but a deleteMany is not one big transaction unless you run it inside one. If it stops halfway, the documents it already removed stay removed. A backup taken before a large delete is worth the few minutes it costs.

When you finish testing, remove the example database.

use deleteDemo
db.dropDatabase()

MongoDB deleteMany is not risky because of what it does, it is risky because of what an empty filter means.

Published by Pinal Dave on SQLAuthority. More of my work at pinaldave.com.


Discover more from SQL Authority with Pinal Dave

Subscribe to get the latest posts sent to your email.

Database, MongoDB, NoSQL, SQL Scripts
Previous Post
MongoDB updateOne: $set, $inc and Upserts Without Surprises
Next Post
SQL to MongoDB: Mapping SELECT, WHERE, GROUP BY and JOIN

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.