← Back to Index

CRUD
Delete

FILE 07_crud_delete
TOPIC deleteOne · deleteMany · findOneAndDelete · drop · TTL
LEVEL Foundation
01
Method Overview
Delete methods at a glance
Overview
MethodDeletesReturnsUse When
deleteOne()First matching document{ acknowledged, deletedCount }Remove one record by filter
deleteMany()All matching documents{ acknowledged, deletedCount }Batch removal by condition
findOneAndDelete()First matching documentThe deleted document (or null)Need the document data after deleting
drop()Entire collectiontrue/falseWipe all docs + indexes + metadata
dropDatabase()Entire databaseok: 1Full cleanup — use with extreme caution
DANGERMongoDB has no foreign key constraints. Deleting a document does NOT cascade to related documents in other collections. Orphaned references are your application's responsibility.
02
deleteOne()
Remove the first matching document
Single
// Signature
db.collection.deleteOne(filter, options)

// Delete by _id (most precise — always matches exactly one)
db.users.deleteOne({ _id: ObjectId("507f1f77bcf86cd799439011") })
// → { acknowledged: true, deletedCount: 1 }

// Delete by field value (removes first match — natural order)
db.users.deleteOne({ email: "spam@example.com" })
// → { acknowledged: true, deletedCount: 1 }

// No match — no error, just deletedCount: 0
db.users.deleteOne({ _id: ObjectId("000000000000000000000000") })
// → { acknowledged: true, deletedCount: 0 }

deleteOne() Returns

FieldMeaning
acknowledgedtrue if write concern was satisfied
deletedCount0 (no match) or 1 (deleted). Never >1 for deleteOne()
TIPAlways delete by _id when possible — it uses the primary index, is always unique, and guarantees exactly one document is deleted.
03
deleteMany()
Bulk removal · empty filter trap · no rollback
Bulk
// Signature
db.collection.deleteMany(filter, options)

// Delete all expired sessions
db.sessions.deleteMany({ expiresAt: { $lt: new Date() } })
// → { acknowledged: true, deletedCount: 47 }

// Delete all documents in a collection (NOT the same as drop!)
db.logs.deleteMany({})
// → Removes all docs but keeps collection + indexes + metadata intact

// Delete by type mismatch (fix bad data)
db.products.deleteMany({ price: { $type: "string" } })

deleteMany() vs drop()

AspectdeleteMany({})drop()
What's removedAll documents onlyDocuments + indexes + metadata + collection itself
SpeedSlower — removes docs one by one, updates indexesMuch faster — single filesystem operation
After operationEmpty collection with indexes still thereCollection no longer exists
Re-insertReady immediately — indexes are already in placeMust recreate collection and all indexes
Use whenKeeping schema/indexes; partial deletion commonFull wipe needed; indexes will change anyway
WARNdeleteMany({}) with an empty filter deletes every document in the collection. Always double-check your filter in production. Consider testing with find({}) first to confirm what will be deleted.
04
findOneAndDelete()
Atomic pop · returns deleted doc · queue pattern
Atomic

Atomically finds, deletes, and returns the deleted document. Unlike deleteOne(), you get the document's data back — essential for job queues or audit logging.

// Signature
db.collection.findOneAndDelete(filter, options)
// options: { sort, projection, maxTimeMS, collation, hint }

// Basic — returns the deleted document (pre-deletion state)
const deleted = db.users.findOneAndDelete({ email: "bob@example.com" })
if (deleted) {
  print(`Deleted user: ${deleted.name}`)  // still have the data!
}
// No match → returns null (not an error)

// Priority job queue — pop highest-priority oldest job atomically
const job = db.jobs.findOneAndDelete(
  { status: "ready" },
  { sort: { priority: -1, createdAt: 1 } }  // highest prio, oldest first
)
// Worker gets job object AND it's removed — no duplicate processing

// With projection — only return specific fields from deleted doc
db.sessions.findOneAndDelete(
  { expired: true },
  { projection: { sessionToken: 1, userId: 1, _id: 0 } }
)
// Returns only sessionToken and userId from the deleted session

findOneAndDelete() vs deleteOne()

AspectdeleteOne()findOneAndDelete()
Returns{ acknowledged, deletedCount }The deleted document (or null)
You get data backNoYes — before deletion
PerformanceFaster (lighter)Slightly slower (must return doc)
Use whenDon't need deleted contentNeed to act on deleted content
Sort optionNo (before MongoDB 8.0)Yes — controls which doc is deleted
NOTEAlways check if (result) before accessing properties. findOneAndDelete() returns null on no match — accessing result.name on null throws "Cannot read property of null".
05
drop() vs dropDatabase()
Collection wipe · database removal · TTL auto-expiry
Structural
// drop() — removes entire collection (docs + indexes + metadata)
db.tempLogs.drop()    // → true (success) or false (collection didn't exist)

// dropDatabase() — removes the ENTIRE current database
use myDatabase
db.dropDatabase()     // → { ok: 1 } — all collections in myDatabase are gone
DANGERNever run dropDatabase() on admin or local — this corrupts replica set configuration and loses all user credentials. Reserved databases: admin, local, config.

TTL Index — Automatic Document Expiry

Instead of manually running deleteMany() for cleanup, use a TTL (Time To Live) index to auto-expire documents.

// Create TTL index: expire sessions 1 hour after createdAt
db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 3600 }  // 3600s = 1 hour
)
// MongoDB deletes expired documents automatically every ~60 seconds
// The field must be a BSON Date type — string dates won't work!

// TTL index use cases:
// - Session stores (expire after inactivity)
// - Password reset tokens (expire after 15 mins)
// - Cache documents (auto-evict stale data)
// - Log rotation (keep only last 30 days)

// Check existing TTL indexes
db.sessions.getIndexes()
// Look for: { "expireAfterSeconds": 3600 } in the index definition
TIPTTL cleanup runs as a background task roughly every 60 seconds. Documents are not deleted at the exact expiry moment — they may linger up to ~60s past expiry. Don't rely on TTL for security-critical exact-time expiry.
06
Edge Cases
No referential integrity · null filter · type matching · orphaned refs
Edge Cases

No Referential Integrity — Orphaned References

// Deleting a user does NOT delete their orders
db.users.deleteOne({ _id: ObjectId("user1") })

// Orders collection still has references to deleted user:
// { _id: "ord1", userId: ObjectId("user1"), total: 999 } ← orphaned!

// MongoDB won't warn you. Application must handle cascading deletes:
function deleteUserAndOrders(userId) {
  db.orders.deleteMany({ userId: ObjectId(userId) })  // delete orders first
  db.users.deleteOne({ _id: ObjectId(userId) })        // then delete user
  // OR: use a multi-document transaction for atomicity
}

null Filter Matches More Than Expected

// { status: null } matches BOTH explicitly null AND missing status field
db.orders.deleteMany({ status: null })
// Deletes orders where status is null AND orders that have no status field!

// To delete ONLY docs where status is explicitly null:
db.orders.deleteMany({ status: { $eq: null, $exists: true } })

// To delete ONLY docs where status field is missing:
db.orders.deleteMany({ status: { $exists: false } })

Type Sensitivity — Won't Match Wrong Type

// If price stored as String "free" and you filter with number:
db.products.deleteMany({ price: { $lt: 0 } })
// Won't delete string "free" — types don't match across comparison operators

// Find and delete type mismatches explicitly:
db.products.deleteMany({ price: { $type: "string" } })

deleteMany() is Not Atomic — No Rollback

// If deleteMany() fails midway, partial deletes are permanent
// Documents deleted before the error are GONE — no automatic rollback

// For all-or-nothing mass deletion, use a transaction:
const session = db.getMongo().startSession()
session.startTransaction()
try {
  db.getSiblingDB("mydb").orders.deleteMany(
    { status: "cancelled" },
    { session }
  )
  db.getSiblingDB("mydb").audit.insertOne(
    { action: "purge_cancelled", timestamp: new Date() },
    { session }
  )
  session.commitTransaction()
} catch (e) {
  session.abortTransaction()  // rolls back everything
} finally {
  session.endSession()
}

Soft Delete Pattern — Don't Delete, Mark Instead

// Hard delete — permanent, no undo
db.users.deleteOne({ _id: userId })

// Soft delete — keep data, mark as deleted (recoverable)
db.users.updateOne(
  { _id: userId },
  {
    $set: { deletedAt: new Date(), isDeleted: true },
    $unset: { sessionToken: "" }   // invalidate session
  }
)
// Active user queries always include: { isDeleted: { $ne: true } }
// Benefits: audit trail, recovery possible, foreign keys remain valid
WARNHard deletes are irreversible — once committed, there is no "undo" in MongoDB without a backup. Use soft deletes (mark isDeleted: true) for user-facing data, and reserve hard deletes for truly temporary or expired data.