36 CRUD: Delete – Permanenz und Vorsicht

Delete-Operationen sind die gefährlichsten CRUD-Operations. Create fügt hinzu, Read ändert nichts, Update modifiziert – aber Delete ist permanent. Ein versehentlicher Mass-Delete kann Produktion-Daten unwiederbringlich vernichten. Ein Typo im Filter – { status: "test" } statt { status: "testing" } – könnte tausende falsche Dokumente löschen. Im Gegensatz zu relationalen Datenbanken mit COMMIT/ROLLBACK haben MongoDB-Deletes keine Built-in-Undo-Funktionalität. Einmal gelöscht, ist restored nur via Backup möglich.

Diese Permanenz erfordert außergewöhnliche Vorsicht. Production-Teams entwickeln oft Defense-in-Depth-Strategies: Soft-Deletes statt Hard-Deletes, explizite Confirmation-Workflows für Mass-Deletes, Backup-Verification vor kritischen Operations, Audit-Logs für alle Deletes. Manche Organizations verbieten deleteMany({}) komplett via Database-Permissions und verlangen stattdessen manuell reviewed Scripts. Dies ist nicht Paranoia, sondern pragmatische Risk-Management.

Dieses Kapitel behandelt Delete-Operationen systematisch – von grundlegenden APIs über Safety-Patterns bis zu Alternatives wie Soft-Deletes und TTL-Indexes. Der Fokus ist nicht nur “wie löscht man”, sondern “wie löscht man sicher und reversibel”.

36.1 deleteOne und deleteMany: Die primären Delete-APIs

MongoDB bietet zwei Delete-Methoden mit analoger Semantik zu Update-Operationen.

deleteOne() – Löscht das erste Match:

const result = db.users.deleteOne({ username: "temporaryTestUser" })
printjson(result)

Dies findet das erste Dokument mit username: "temporaryTestUser" und löscht es. Die Response zeigt Success-Status:

{
  acknowledged: true,
  deletedCount: 1
}

deletedCount: 1 bedeutet, ein Dokument wurde gelöscht. Wenn kein Dokument matched, ist deletedCount: 0 – kein Error, nur keine Aktion. Dies ist wichtig für idempotente Operations – mehrmaliges Aufrufen schadet nicht.

Warum deleteOne() bevorzugen:

Für spezifische Deletes – “Delete this user”, “Remove this order” – ist deleteOne() safer als deleteMany(). Selbst wenn der Filter versehentlich multiple Dokumente matched (etwa durch Typo), wird nur eines gelöscht. Dies limitiert den Schaden eines Fehlers.

deleteMany() – Löscht alle Matches:

const result = db.logs.deleteMany({ 
  createdAt: { $lt: new Date("2024-01-01") }
})
printjson(result)

Dies löscht alle Log-Dokumente älter als 2024-01-01. Die Response zeigt die totale Anzahl:

{
  acknowledged: true,
  deletedCount: 45231
}

45.231 Dokumente gelöscht – ein signifikanter Bulk-Delete. Für solche Operations sollte man extreme Vorsicht walten lassen.

Die gefährlichste Query: deleteMany({})

Ein Empty-Filter matched alle Dokumente:

db.users.deleteMany({})
// Löscht ALLE Users!

Dies ist äquivalent zu DELETE FROM users ohne WHERE-Clause in SQL – eine katastrophale Operation für Production-Daten. MongoDB warnt nicht oder fragt nach Confirmation. Es führt einfach aus. Die einzige Verteidigung: Sorgfältige Code-Review und möglicherweise Database-Permissions, die deleteMany({}) verbieten.

36.2 Safety-Pattern: Dry-Run via countDocuments

Bevor man ein deleteMany() ausführt, sollte man prüfen, wie viele Dokumente betroffen sind:

// Schritt 1: Count
const affectedCount = db.logs.countDocuments({ 
  createdAt: { $lt: new Date("2024-01-01") }
})
print(`Would delete ${affectedCount} documents`)

// Schritt 2: Sample-Check
print("Sample documents to be deleted:")
db.logs.find({ 
  createdAt: { $lt: new Date("2024-01-01") }
}).limit(5).forEach(doc => printjson(doc))

// Schritt 3: Manual Confirmation (in Script)
const confirm = true  // Set via user input oder Environment Variable
if (!confirm) {
  print("Delete cancelled")
  quit(0)
}

// Schritt 4: Actual Delete
if (affectedCount > 0) {
  const result = db.logs.deleteMany({ 
    createdAt: { $lt: new Date("2024-01-01") }
  })
  print(`Deleted ${result.deletedCount} documents`)
}

Dieser Workflow verhindert Überraschungen. Man sieht die Anzahl und Samples vor dem Delete. Für Production-Scripts sollte dies Standard sein.

Mit Thresholds:

Für automatisierte Scripts kann man Thresholds nutzen:

const affectedCount = db.logs.countDocuments({ status: "archived" })

if (affectedCount > 10000) {
  print(`ERROR: Would delete ${affectedCount} documents - exceeds threshold!`)
  quit(1)
}

db.logs.deleteMany({ status: "archived" })

Dies verhindert versehentliche Mass-Deletes durch Filter-Bugs. Wenn plötzlich 100k Dokumente matched statt erwartetem 1k, stoppt das Script.

36.3 Soft Deletes: Marking statt Removing

Viele Production-Systeme nutzen Soft-Deletes – Dokumente werden nicht physisch gelöscht, sondern als gelöscht markiert:

// Statt Delete
db.users.updateOne(
  { username: "alice" },
  { $set: { 
      deleted: true,
      deletedAt: new Date(),
      deletedBy: "admin@example.com"
  }}
)

// Queries excluden deleted Dokumente
db.users.find({ 
  deleted: { $ne: true }  // Oder: { $exists: false }
})

Vorteile von Soft-Deletes:

  1. Reversibilität: Versehentliche Deletes können “undeleted” werden durch $unset: { deleted: "", deletedAt: "" }
  2. Audit-Trail: Man sieht wann und von wem gelöscht wurde
  3. Compliance: Manche Regulations verlangen Delete-History
  4. Graceful Degradation: Bei Bugs oder Daten-Corruption kann man Deletes revertn ohne Backup-Restore

Nachteile:

  1. Storage-Wachstum: Deleted Dokumente konsumieren Speicher
  2. Query-Komplexität: Jede Query muss deleted: { $ne: true } inkludieren
  3. Index-Bloat: Indexes enthalten deleted Dokumente

Für die Storage-Problematik kann man periodic Cleanup implementieren:

// Permanent delete soft-deleted documents older than 90 days
const ninetyDaysAgo = new Date()
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90)

db.users.deleteMany({
  deleted: true,
  deletedAt: { $lt: ninetyDaysAgo }
})

Dies balanciert Reversibilität (90-Tage-Window) mit Storage-Management.

Partial Index für Performance:

Um Query-Performance zu optimieren trotz Soft-Deletes, nutzt man einen Partial-Index, der deleted Dokumente excludiert:

db.users.createIndex(
  { email: 1 },
  { partialFilterExpression: { deleted: { $ne: true } } }
)

Queries auf active Users nutzen diesen schlanken Index, nicht einen aufgeblähten Index mit allen gelöschten Dokumenten.

36.4 findOneAndDelete: Atomic Read-Delete

Manchmal braucht man das Dokument zurück beim Delete – etwa um zu loggen was gelöscht wurde oder um finale Cleanup-Actions durchzuführen. findOneAndDelete() returned das gelöschte Dokument:

const deletedUser = db.users.findOneAndDelete({ username: "testuser" })

if (deletedUser) {
  print(`Deleted user: ${deletedUser.username}, email: ${deletedUser.email}`)
  // Log to external system, trigger cleanup, etc.
} else {
  print("No user found to delete")
}

Dies ist atomar – das Read und Delete passieren als eine Operation, keine Race-Condition zwischen “find then delete”. Für Audit-Logging oder Cascade-Deletes ist dies essentiell.

Mit Projection:

Man kann limitieren, welche Felder returned werden:

const deletedUser = db.users.findOneAndDelete(
  { username: "testuser" },
  { projection: { username: 1, email: 1, _id: 0 } }
)

Dies spart Netzwerk-Bandwidth wenn das Dokument groß ist, aber man nur wenige Felder fürs Logging braucht.

36.5 Bulk Deletes: Performance bei großen Mengen

Für Bulk-Deletes auf sehr großen Collections kann Performance zum Problem werden. Ein deleteMany() auf Millionen Dokumente kann Minuten dauern und das System belasten.

Batched Deletes:

Statt ein riesiges deleteMany(), delete in Batches:

let deletedTotal = 0
const batchSize = 1000

while (true) {
  const result = db.logs.deleteMany(
    { createdAt: { $lt: new Date("2023-01-01") } },
    { limit: batchSize }  // MongoDB 6.0+ unterstützt limit in deleteMany
  )
  
  deletedTotal += result.deletedCount
  print(`Deleted ${deletedTotal} total...`)
  
  if (result.deletedCount < batchSize) {
    break  // Keine Dokumente mehr zu löschen
  }
  
  sleep(100)  // Kurze Pause zwischen Batches
}

print(`Total deleted: ${deletedTotal}`)

Dies limitiert die Last auf das System – jeder Batch ist eine kleinere Operation, das System kann zwischen Batches andere Queries bedienen. Die sleep() ist optional aber höflich für concurrent Workloads.

Chunked Delete für extreme Scale:

Für Multi-Million-Dokument-Deletes kann man Chunking basierend auf Index-Ranges nutzen:

// Delete in _id ranges
const minId = db.logs.find().sort({ _id: 1 }).limit(1).toArray()[0]._id
const maxId = db.logs.find().sort({ _id: -1 }).limit(1).toArray()[0]._id

const chunkSize = 10000  // IDs per chunk (approximat)
let currentId = minId

while (currentId < maxId) {
  const nextId = ObjectId(
    (parseInt(currentId.toString().slice(0, 8), 16) + chunkSize)
      .toString(16).padStart(8, '0') + '0000000000000000'
  )
  
  const result = db.logs.deleteMany({
    _id: { $gte: currentId, $lt: nextId },
    createdAt: { $lt: new Date("2023-01-01") }
  })
  
  print(`Deleted ${result.deletedCount} documents in chunk`)
  currentId = nextId
}

Dies ist komplex aber erlaubt parallele Deletes auf verschiedenen Shard-Key-Ranges in Sharded-Clusters.

36.6 drop() vs. deleteMany({}): Collection löschen

Für das komplette Löschen einer Collection gibt es zwei Optionen:

// Option 1: deleteMany({})
db.tempData.deleteMany({})

// Option 2: drop()
db.tempData.drop()

deleteMany({}): - Löscht alle Dokumente, behält Collection und Indexes - Langsam bei großen Collections (muss jedes Dokument löschen) - Kann partial failures haben bei very large operations

drop(): - Löscht Collection, alle Dokumente und alle Indexes - Sehr schnell (Filesystem-Level-Delete) - Atomar – entweder komplett dropped oder nicht

Für temporäre Collections oder komplettes Cleanup ist drop() effizienter. Für Collections, die man behalten will (etwa weil sie Schema-Validation oder spezielle Indexes hat), nutzt man deleteMany({}).

Achtung bei Sharded Collections:

In Sharded-Clusters ist drop() auf sharded Collections eine zwei-Phasen-Operation und kann während dem Drop keine neuen Writes akzeptieren. Für sehr große sharded Collections sollte man dies in Maintenance-Windows planen.

36.7 TTL Indexes: Automatische Deletes

Für Collections mit zeitbasierten Daten – Logs, Sessions, Temporary-Data – kann man TTL (Time-To-Live) Indexes nutzen für automatische Deletes:

// Auto-delete documents 24 hours after createdAt
db.sessions.createIndex(
  { createdAt: 1 },
  { expireAfterSeconds: 86400 }  // 24 hours
)

// Insert document
db.sessions.insertOne({
  sessionId: "SESSION-123",
  userId: "USER-456",
  createdAt: new Date()  // TTL starts from this timestamp
})

MongoDB’s Background-Task prüft periodisch (alle 60 Sekunden) den TTL-Index und löscht expired Dokumente automatisch. Dies ist effizienter und zuverlässiger als Cron-Jobs oder Application-Logic.

TTL auf Date-Field:

Der TTL-Index muss auf einem Date-Field sein. Wenn das Dokument kein passendes Date-Field hat oder es null/undefined ist, wird es nie gelöscht.

Exact Deletion Time:

TTL-Deletes sind nicht instant – sie passieren typischerweise innerhalb 60-120 Sekunden nach Expiration. Für Anwendungen, die sofortige Deletion brauchen, ist TTL nicht geeignet.

TTL in Sharded Clusters:

TTL-Indexes funktionieren in Sharded-Clusters, aber jeder Shard hat seinen eigenen Background-Deletion-Thread. Koordination ist nicht nötig – jeder Shard löscht seine eigenen expired Dokumente.

36.8 Performance-Implikationen: Indexes und Deletes

Deletes nutzen Indexes zum Finden der zu löschenden Dokumente, genauso wie Queries:

// Ohne Index: COLLSCAN
db.logs.deleteMany({ level: "debug" })
// Scannt alle Dokumente

// Mit Index: IXSCAN
db.logs.createIndex({ level: 1 })
db.logs.deleteMany({ level: "debug" })
// Nutzt Index, viel schneller

Aber Deletes haben einen zusätzlichen Overhead: Alle Indexes müssen updated werden. Jedes Dokument, das gelöscht wird, muss aus allen Indexes entfernt werden. Bei vielen Indexes ist dies teuer.

Index-Consideration für Bulk-Deletes:

Für massive Bulk-Deletes auf Collections mit vielen Indexes kann es effizienter sein, Indexes temporär zu droppen:

// 1. Backup index definitions
const indexes = db.logs.getIndexes()
printjson(indexes)

// 2. Drop indexes (außer _id)
db.logs.dropIndex("level_1")
db.logs.dropIndex("createdAt_1")
db.logs.dropIndex("userId_1")

// 3. Bulk delete (viel schneller ohne Index-Maintenance)
db.logs.deleteMany({ createdAt: { $lt: new Date("2023-01-01") } })

// 4. Recreate indexes
db.logs.createIndex({ level: 1 })
db.logs.createIndex({ createdAt: 1 })
db.logs.createIndex({ userId: 1 })

Dies ist ein fortgeschrittenes Pattern für extreme Bulk-Deletes (Millionen+ Dokumente). Der Trade-off: Während Indexes fehlen, sind Queries langsam. Für Live-Production muss man dies in Maintenance-Windows planen.

36.9 Audit-Logging und Compliance

Für Compliance oder Security-Audits will man oft tracken, wer was wann gelöscht hat. MongoDB hat keine Built-in-Delete-Logging, aber man kann es implementieren.

Change Streams für Delete-Tracking:

// Watch for delete operations
const changeStream = db.users.watch([
  { $match: { operationType: "delete" } }
])

changeStream.on("change", (change) => {
  db.auditLog.insertOne({
    operation: "delete",
    collection: "users",
    documentId: change.documentKey._id,
    timestamp: new Date(),
    user: change.ns  // In production: extract from session context
  })
})

Change Streams erlauben real-time Notification über Deletes. Man kann ein separates Audit-Collection maintainen mit allen Delete-Events.

Pre-Delete-Logging mit findOneAndDelete:

function deleteWithAudit(collection, filter, auditUser) {
  const deleted = db[collection].findOneAndDelete(filter)
  
  if (deleted) {
    db.auditLog.insertOne({
      operation: "delete",
      collection: collection,
      deletedDocument: deleted,
      user: auditUser,
      timestamp: new Date()
    })
    
    print(`Deleted and logged: ${deleted._id}`)
  }
}

deleteWithAudit("users", { username: "testuser" }, "admin@example.com")

Dies saved das komplette gelöschte Dokument in der Audit-Collection. Storage-intensive, aber maximale Traceability.

36.10 Backup-Strategien vor kritischen Deletes

Für wirklich kritische Deletes – etwa Mass-Deletes in Production oder Schema-Migrations mit Deletes – sollte man Backups vor der Operation nehmen.

Schnelles Collection-Backup:

// Export zu temporary Collection
db.users.aggregate([
  { $match: { status: "to_be_deleted" } },
  { $out: "users_delete_backup_2024_01_06" }
])

print("Backup created in users_delete_backup_2024_01_06")

// Perform delete
db.users.deleteMany({ status: "to_be_deleted" })

// Keep backup for 7 days, then drop

Die $out-Aggregation-Stage erstellt eine neue Collection mit den zu löschenden Dokumenten. Wenn etwas schiefgeht, kann man sie zurück-mergen.

mongodump für Filesystem-Backup:

Für sehr große Deletes:

# Backup entire database
mongodump --db production --out /backups/before-delete-$(date +%Y%m%d)

# Perform delete operation

# If needed, restore
mongorestore --db production /backups/before-delete-20240106

Dies ist langsamer aber robuster – kompletter Filesystem-Backup inkl. Indexes.

36.11 Die Psychologie von Deletes: Cultural Practices

In vielen Organizations sind Deletes so gefährlich, dass sie spezielle Processes erfordern:

Diese Practices sind nicht übertrieben. Ein versehentlicher Delete kann Millionen-Dollar-Schaden verursachen – lost Customer-Data, Compliance-Violations, Reputation-Damage. Die Kosten dieser Processes sind minimal verglichen mit dem Risiko.

Die folgende Tabelle fasst Delete-Strategien zusammen:

Strategie Reversibilität Performance Compliance Use-Case
Hard Delete Nein (nur via Backup) Schnell Schwierig Temporäre Daten
Soft Delete Ja (instant) Langsamer (Queries complex) Exzellent User Data
TTL Index Nein Automatisch Moderat Sessions, Logs
Backup + Delete Ja (via Restore) Langsam (2 ops) Gut Critical Mass-Deletes
Archive Collection Ja (manual merge) Moderat Gut Compliance Data

Delete-Operationen sind gefährlich und permanent. Die Production-Best-Practice ist maximale Vorsicht – Dry-Runs mit countDocuments, Soft-Deletes wo möglich, Backups vor kritischen Operations, Audit-Logging für Compliance, und kulturelle Practices wie Peer-Review und Approval-Workflows. MongoDB’s APIs machen Deletes einfach, aber einfach ≠ sicher. Ein einziger deleteMany({}) mit Typo kann katastrophal sein. Die Verteidigung ist nicht technisch allein, sondern eine Kombination von sorgfältiger Code-Review, robustem Testing, Backup-Discipline und organisatorischen Safeguards. Mit diesen Practices werden Deletes von gefährlichsten Operation zu kontrollierten, auditbaren und reversiblen Workflow.