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 removes the first document matching the filter:
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:
db.products.deleteOne({ _id: 'SKU-1001' })deleteMany removes every matching document, so treat its filter with respect:
// 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:
db.tempData.deleteMany({}) // wipes the collection's documentsThe collection itself, along with its indexes, survives. To remove the collection entirely, use db.tempData.drop(), which is faster and also discards the indexes.
Because deletes are irreversible, verify the blast radius first by running the same filter through find or countDocuments:
db.products.countDocuments({ status: 'discontinued' }) // 12 - looks right
db.products.find({ status: 'discontinued' }, { name: 1 })
db.products.deleteMany({ status: 'discontinued' }) // now deleteThis 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.
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:
const job = db.jobs.findOneAndDelete(
{ status: 'pending' },
{ sort: { priority: -1, createdAt: 1 } }
)Two workers running this concurrently can never grab the same job.
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:
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.
Many applications never physically delete user-facing data. Instead they mark it:
db.posts.updateOne({ _id: postId }, { $set: { deletedAt: new Date() } })
db.posts.find({ deletedAt: { $exists: false } }) // normal readsSoft 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.
CRUD is now complete. Next we make it fast: the following lesson explores indexes and how to read query execution plans.