Delete Documents

MongoDB · Lesson 12 of 28 7 min read Updated

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:

MongoDB Shell
db.orders.countDocuments({ customerId: 8 })
db.customers.deleteOne({ _id: 8 })
db.customers.deleteOne({ _id: 8 })
Output
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:

MongoDB Shell
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()
Output
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6ae44012c3f5e8a91d2b7a08'),
    '1': ObjectId('6ae44012c3f5e8a91d2b7a09'),
    '2': ObjectId('6ae44012c3f5e8a91d2b7a0a'),
    '3': ObjectId('6ae44012c3f5e8a91d2b7a0b')
  }
}
2
{ acknowledged: true, deletedCount: 2 }
2

Look 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:

MongoDB Shell
db.carts.deleteMany({})
db.carts.drop()
Output
{ acknowledged: true, deletedCount: 2 }
true

Watch 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:

MongoDB Shell
db.orders.findOneAndDelete(
  { status: 'cancelled' },
  { sort: { date: 1 }, projection: { customerId: 1, total: 1 } }
)
Output
{ _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:

MongoDB Shell
db.products.updateOne({ _id: 106 }, { $set: { deletedAt: ISODate('2026-10-30') } })
db.products.countDocuments({ deletedAt: { $exists: false } })
Output
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}
9

The 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() or countDocuments() with the same filter first.
  • Using deleteOne() with a filter that matches many. It removes an arbitrary one of them. Delete by _id, or use findOneAndDelete() 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 and deleteMany(filter) removes all matches; both return deletedCount.
  • An empty filter {} matches everything, so check filters with countDocuments() before deleting.
  • drop() removes a whole collection with its indexes; deleteMany({}) keeps the empty collection.
  • findOneAndDelete() removes one document and returns it, with optional sort and projection.
  • Soft deletes mark documents with a field such as deletedAt instead 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
MongoDB Shell
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?

2. Which method removes one document and returns it to you?

3. What happens to a customer's orders when you delete the customer document?

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