Delete Documents
Deleting is the easiest operation to write and the hardest to undo. Abandoned shopping carts, expired login sessions and test data all need cleaning up, but one wrong filter can wipe out real orders in a second. MongoDB has no "are you sure?" prompt and no recycle bin, so good habits matter more here than anywhere else.
In this lesson you will delete one or many documents, delete and read in one step, check filters before you run them, and see two alternatives to deleting at all: soft deletes and automatic expiry. Start with freshly loaded data and use shop.
deleteOne()
deleteOne(filter) removes the first document that matches the filter. Isha (customer 8) asks for her account to be closed. Because MongoDB does not enforce links between collections, first check that she has no orders that would point at a missing customer:
db.orders.countDocuments({ customerId: 8 })
db.customers.deleteOne({ _id: 8 })
db.customers.deleteOne({ _id: 8 })0
{ acknowledged: true, deletedCount: 1 }
{ acknowledged: true, deletedCount: 0 }The second deleteOne() found nothing, so deletedCount is 0. That is not an error; your code should check deletedCount if it needs to know whether something was removed.
Deleting by _id is the safest kind of delete, because _id matches at most one document. If a filter could match several documents, deleteOne() removes whichever it finds first, which is rarely what you want.
deleteMany()
deleteMany(filter) removes every matching document. Create a few shopping carts, then clear out the ones nobody has touched since before October:
db.carts.insertMany([
{ customerId: 2, items: [105], updatedAt: ISODate('2026-09-12') },
{ customerId: 4, items: [107, 104], updatedAt: ISODate('2026-10-25') },
{ customerId: 5, items: [101], updatedAt: ISODate('2026-08-30') },
{ customerId: 6, items: [110], updatedAt: ISODate('2026-10-29') }
])
db.carts.countDocuments({ updatedAt: { $lt: ISODate('2026-10-01') } })
db.carts.deleteMany({ updatedAt: { $lt: ISODate('2026-10-01') } })
db.carts.countDocuments(){
acknowledged: true,
insertedIds: {
'0': ObjectId('6ae44012c3f5e8a91d2b7a08'),
'1': ObjectId('6ae44012c3f5e8a91d2b7a09'),
'2': ObjectId('6ae44012c3f5e8a91d2b7a0a'),
'3': ObjectId('6ae44012c3f5e8a91d2b7a0b')
}
}
2
{ acknowledged: true, deletedCount: 2 }
2Look at the pattern: run countDocuments() with the same filter first, confirm the number is what you expect, and only then delete. The two September and August carts are gone and the two recent ones remain.
Deleting everything
deleteMany({}) has an empty filter, so it deletes every document in the collection. The collection itself and its indexes stay. If you want to get rid of the collection completely, drop() is faster and also removes the indexes:
db.carts.deleteMany({})
db.carts.drop(){ acknowledged: true, deletedCount: 2 }
trueWatch out for a missing filter in application code. If a variable that should hold a filter ends up as {} because of a bug, deleteMany(filter) deletes the whole collection. Validate input before building filters, and never pass user input directly as a filter.
findOneAndDelete(): delete and get the document
findOneAndDelete() removes one document and returns it, in one atomic step. This is useful when you need the deleted data, for example to write it to an archive or a log. It also accepts sort and projection options, so you control which document goes:
db.orders.findOneAndDelete(
{ status: 'cancelled' },
{ sort: { date: 1 }, projection: { customerId: 1, total: 1 } }
){ _id: 1005, customerId: 4, total: 2499 }The cancelled order 1005 was removed and returned. The same method is the classic way to build a simple job queue: each worker takes and removes the oldest job, and no two workers can get the same one. It returns null when nothing matches.
Soft deletes: hiding instead of removing
Many applications never really delete important records. Instead they mark them as deleted with a field such as deletedAt, and normal queries skip marked documents. This keeps history, allows an "undo" and keeps old orders that reference the record meaningful. The Laptop Stand is discontinued:
db.products.updateOne({ _id: 106 }, { $set: { deletedAt: ISODate('2026-10-30') } })
db.products.countDocuments({ deletedAt: { $exists: false } }){
acknowledged: true,
insertedId: null,
matchedCount: 1,
modifiedCount: 1,
upsertedCount: 0
}
9The product still exists for order 1010, which contains it, but the catalogue query { deletedAt: { $exists: false } } now finds only the 9 active products. The cost is that every query must remember the extra condition. In the Node.js world, libraries such as Mongoose plugins can add it automatically.
Automatic deletes with TTL indexes
For data that should simply expire, such as login sessions, OTP codes or abandoned carts, you do not need a cleanup script at all. A TTL (time to live) index tells MongoDB to delete documents a set number of seconds after the date in a field. A background task on the server removes expired documents roughly once a minute. You will create one in Indexes.
When you have finished, reload the shop data with load('shop-data.js').
Common mistakes
- Deleting without testing the filter. Always run
find()orcountDocuments()with the same filter first. - Using
deleteOne()with a filter that matches many. It removes an arbitrary one of them. Delete by_id, or usefindOneAndDelete()with a sort. - Forgetting related documents. MongoDB has no foreign keys or cascading deletes. Deleting a customer does not touch their orders; decide what should happen to them first.
- Relying on deletes to free disk space at once. Space from deleted documents is reused for new data but is not always returned to the operating system immediately.
Key points
deleteOne(filter)removes the first match anddeleteMany(filter)removes all matches; both returndeletedCount.- An empty filter
{}matches everything, so check filters withcountDocuments()before deleting. drop()removes a whole collection with its indexes;deleteMany({})keeps the empty collection.findOneAndDelete()removes one document and returns it, with optionalsortandprojection.- Soft deletes mark documents with a field such as
deletedAtinstead of removing them. - TTL indexes delete expired documents automatically.
Exercise
Starting from fresh data, the shop decides to remove reviews older than a year and to archive cancelled orders. (1) Create a reviews collection with three reviews dated 2025-06-01, 2025-12-15 and 2026-09-20, then delete those dated before 2025-10-30 after checking the count. (2) Move every cancelled order into an archivedOrders collection: copy them first, then delete them from orders.
Show solution
db.reviews.insertMany([
{ productId: 101, stars: 5, date: ISODate('2025-06-01') },
{ productId: 103, stars: 4, date: ISODate('2025-12-15') },
{ productId: 105, stars: 3, date: ISODate('2026-09-20') }
])
db.reviews.countDocuments({ date: { $lt: ISODate('2025-10-30') } })
db.reviews.deleteMany({ date: { $lt: ISODate('2025-10-30') } })
db.archivedOrders.insertMany(db.orders.find({ status: 'cancelled' }).toArray())
db.orders.deleteMany({ status: 'cancelled' })The count is 1, so one review is deleted. The archive step copies order 1005 with its original _id, then removes it from orders. Copy first and delete second, so a failure between the steps can never lose the order; for a guarantee that both happen together, use a transaction.
Quick Quiz
1. What does db.orders.deleteMany({}) do?
An empty filter matches all documents; drop() is what removes the collection itself.
2. Which method removes one document and returns it to you?
findOneAndDelete() deletes and returns the document atomically; deleteOne() only returns a count.
3. What happens to a customer's orders when you delete the customer document?
MongoDB does not enforce references between collections, so your application must handle related data.
Learn MongoDB with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
