Deleting Documents

beginner
10 min

Deleting Documents

Deletion is the simplest CRUD operation and also the most dangerous, because there is no undo. In this CodeYourCraft lesson you will learn deleteOne and deleteMany, how to verify a filter before running it, how TTL indexes expire data automatically, and why many production systems prefer soft deletes.

deleteOne: Remove a Single Document

deleteOne removes the first document matching the filter:

javascript
use shop db.customers.deleteOne({ email: 'arjun@example.com' }) // { acknowledged: true, deletedCount: 1 }

If several documents match, only one, typically the first in natural order, is removed. For predictable behavior, delete by _id whenever possible, since _id is unique:

javascript
db.products.deleteOne({ _id: 'SKU-1001' })

deleteMany: Remove All Matches

deleteMany removes every matching document, so treat its filter with respect:

javascript
// Remove all discontinued products db.products.deleteMany({ status: 'discontinued' }) // Remove logs older than 90 days const cutoff = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) db.logs.deleteMany({ createdAt: { $lt: cutoff } })

An empty filter deletes every document in the collection:

javascript
db.tempData.deleteMany({}) // wipes the collection's documents

The collection itself, along with its indexes, survives. To remove the collection entirely, use db.tempData.drop(), which is faster and also discards the indexes.

The Golden Rule: Count Before You Delete

Because deletes are irreversible, verify the blast radius first by running the same filter through find or countDocuments:

javascript
db.products.countDocuments({ status: 'discontinued' }) // 12 - looks right db.products.find({ status: 'discontinued' }, { name: 1 }) db.products.deleteMany({ status: 'discontinued' }) // now delete

This three-step habit has saved countless production databases. Combine it with regular backups, because a typo in a deleteMany filter is one of the most common causes of data loss.

findOneAndDelete

When you need the removed document back, findOneAndDelete deletes and returns it in one atomic operation, which is perfect for popping tasks off a work queue:

javascript
const job = db.jobs.findOneAndDelete( { status: 'pending' }, { sort: { priority: -1, createdAt: 1 } } )

Two workers running this concurrently can never grab the same job.

Automatic Expiry with TTL Indexes

For data with a natural lifespan, such as sessions, OTPs, and caches, let MongoDB delete it for you with a TTL (time-to-live) index on a date field:

javascript
db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })

A background task runs roughly once a minute and removes documents whose createdAt is more than an hour old. No cron jobs, no cleanup scripts.

Soft Deletes: An Alternative Pattern

Many applications never physically delete user-facing data. Instead they mark it:

javascript
db.posts.updateOne({ _id: postId }, { $set: { deletedAt: new Date() } }) db.posts.find({ deletedAt: { $exists: false } }) // normal reads

Soft deletes preserve history, enable undo features, and simplify audits, at the cost of extra storage and a filter on every query. Choose them for user content; use hard deletes and TTL for logs and sessions.

Key Takeaways

  • deleteOne removes the first match; deleteMany removes all matches.
  • An empty filter with deleteMany empties the collection; drop() removes it entirely.
  • Always count or preview matches before a destructive delete.
  • TTL indexes expire session-like data automatically.
  • Soft deletes trade storage for recoverability and auditability.

CRUD is now complete. Next we make it fast: the following lesson explores indexes and how to read query execution plans.

Deleting Documents - MongoDB | CodeYourCraft | CodeYourCraft