Databases degenerieren über Zeit. Writes und Deletes hinterlassen Fragmentierung – Gaps im Storage, ineffiziente Index-Structures, unbalanced Chunks in Sharded-Clusters. Eine fresh Database mit 1 Million Dokumenten auf 500 MB Storage wächst nach Monaten von Insert/Update/Delete-Operations auf 800 MB – 300 MB Overhead durch Fragmentierung. Queries werden langsamer, weil Indexes fragmentiert sind. Sharded-Clusters entwickeln Hotspots, weil Chunks unequal distributed sind. Diese Degradation ist graduell und oft unbemerkt, bis Performance-Problems offensichtlich werden.
Reorganisation ist der Prozess, eine Database zu “defragmentieren” – Speicher zu reclaimen, Indexes zu rebuilden, Chunks zu rebalancen. Dies ist nicht Daily-Maintenance, sondern Periodic-Optimization – etwa quarterly oder when Performance-Degradation detektiert wird. Aber Reorganisation ist nicht kostenlos – sie erfordert I/O, CPU, manchmal Downtime. Die Kunst ist zu wissen wann Reorganisation nötig ist, welche Method zu nutzen, und wie Impact zu minimieren.
Dieses Kapitel behandelt Database-Maintenance systematisch – von Fragmentierung-Detection über Compaction-Strategies bis zu Shard-Balancing. Der Fokus ist auf Production-Safe-Operations mit Minimal-Downtime.
MongoDB’s WiredTiger-Storage-Engine ist Log-Structured – es schreibt nie in-place. Jedes Update oder Delete markiert alte Daten als “unused”, schreibt neue Version an anderen Platz. Dies ist gut für Concurrency (keine Locks), aber führt zu Fragmentierung.
Example-Scenario:
// Initial Insert: 1M Dokumente
for (let i = 0; i < 1000000; i++) {
db.logs.insertOne({
userId: randomUser(),
message: "Login successful",
timestamp: new Date()
})
}
// Storage: 500 MB
// Nach 1 Monat: 50% Dokumente updated
for (let i = 0; i < 500000; i++) {
db.logs.updateOne(
{ userId: randomUser() },
{ $set: { message: "Updated message" } }
)
}
// Storage: 750 MB (original 500 MB + 250 MB neue Versionen)
// Aber: Nur 500 MB Data ist "live", 250 MB ist "dead"Die 250 MB “dead” Data ist reclaimable – aber MongoDB reclaims sie nicht automatisch (außer via normal Reuse für neue Writes). Über Zeit akkumuliert diese Fragmentierung.
Fragmentierung-Detection:
const stats = db.logs.stats()
print(`Storage Size: ${stats.storageSize / (1024**3)} GB`)
print(`Data Size: ${stats.size / (1024**3)} GB`)
print(`Overhead: ${((stats.storageSize - stats.size) / stats.storageSize * 100).toFixed(2)}%`)Output:
Storage Size: 0.75 GB
Data Size: 0.50 GB
Overhead: 33.33%
Ein 33% Overhead ist signifikant – etwa ein Drittel des Storage ist wasted. Für Production-Systems mit TB-scale kann dies hunderte GB wasted Storage bedeuten.
Performance-Impact:
Fragmentierung beeinträchtigt Performance subtil:
Der Impact ist graduell – nicht catastrophic, aber noticeable. Eine Query, die 50ms dauerte, dauert jetzt 80ms. Nicht dramatisch einzeln, aber bei tausenden Queries summiert es sich.
Der compact-Command defragmentiert eine Collection
in-place – rewrites Data compact, reclaims unused Space.
db.runCommand({ compact: "logs" })Was compact macht:
Duration:
Compact dauert proportional zur Collection-Size:
| Collection-Size | Compact-Duration |
|---|---|
| 10 GB | ~5-10 minutes |
| 100 GB | ~30-60 minutes |
| 1 TB | ~4-8 hours |
Blocking-Behavior:
Kritisch: compact blocked die Collection während
Execution. Reads und Writes auf die Collection sind blocked. Dies ist
inakzeptabel für Production-Databases mit 24/7-Uptime-Requirements.
Workaround für Replica-Sets:
Für Replica-Sets kann man compact Rolling durchführen:
# 1. Compact Secondary 1
mongosh --host secondary1:27017 --eval "db.logs.compact()"
# Wait für Completion
# 2. Compact Secondary 2
mongosh --host secondary2:27017 --eval "db.logs.compact()"
# 3. Step-Down Primary
mongosh --host primary:27017 --eval "rs.stepDown()"
# New Primary ist elected
# 4. Compact old Primary (jetzt Secondary)
mongosh --host old-primary:27017 --eval "db.logs.compact()"Dies compacts alle Nodes ohne Primary-Downtime. Während Secondary-Compaction läuft, handelt der Primary alle Traffic. Nach Step-Down wird ein compacted-Secondary der neue Primary.
Space-Requirements:
compact braucht temporary Disk-Space – etwa 1.5x
Collection-Size. Für eine 100 GB Collection braucht man ~150 GB free
Space. Ohne genug Space failed compact.
Use-Case-Decision:
Compact ist sinnvoll wenn: - Overhead > 30% - Collection ist read-heavy (weniger Write-Impact während Compact) - Disk-Space ist teuer - Man kann Rolling-Compaction in Replica-Set durchführen
Compact ist NICHT sinnvoll wenn: - Collection ist write-heavy (Fragmentierung kehrt schnell zurück) - Disk-Space ist günstig - Downtime ist inakzeptabel (Standalone-Databases)
repairDatabase ist wie compact, aber für
die gesamte Database:
db.runCommand({ repairDatabase: 1 })Dies compacts alle Collections in der Database und rebuilds alle
Indexes. Der Impact ist dramatischer als compact – es kann
Stunden oder Tage dauern für große Databases, und blocked die gesamte
Database.
Use-Case:
repairDatabase ist primär für Disaster-Recovery – wenn
eine Database corrupted ist (etwa nach unclean Shutdown). Für normale
Maintenance ist es zu invasive. Prefer compact auf einzelne
Collections statt repairDatabase.
Deprecation-Note:
MongoDB hat repairDatabase in neueren Versions
deprecated für Production-Use. Die empfohlene Alternative: mongodump +
drop + mongorestore, oder Rolling-Compact in Replica-Sets.
Für extreme Fragmentierung oder wenn compact nicht ausreicht, ist die Nuclear-Option: Export alle Daten, drop Collection, reimport.
# 1. Export
mongodump --uri="mongodb://host/mydb" --collection=logs --out=/backup/logs-export
# 2. Drop Collection
mongosh --eval "db.logs.drop()"
# 3. Reimport
mongorestore --uri="mongodb://host/mydb" /backup/logs-export/mydb/logs.bsonDer Reimport schreibt Data fresh und compact – zero Fragmentierung. Alle Indexes werden neu created.
Duration:
Export-Reimport ist langsamer als compact:
| Collection-Size | Export-Reimport-Duration |
|---|---|
| 10 GB | ~15-30 minutes |
| 100 GB | ~2-4 hours |
| 1 TB | ~12-24 hours |
Downtime:
Anders als compact kann man Export-Reimport mit zero-downtime durchführen via Replica-Sets:
# 1. Remove Secondary from Replica-Set
rs.remove("secondary1:27017")
# 2. Export-Reimport auf removed Secondary
# (Details wie oben)
# 3. Re-Add Secondary zu Replica-Set
rs.add("secondary1:27017")
# 4. Wait für Initial-Sync (Secondary repliziert from Primary)
# 5. Repeat für andere Secondaries und PrimaryUse-Case:
Export-Reimport ist sinnvoll wenn: - Extreme Fragmentierung (> 50% Overhead) - Collection-Schema ändert (Opportunity für Schema-Migration) - Indexes müssen redesigned werden - Man kann Rolling-Process in Replica-Set nutzen
Indexes fragmentieren ähnlich wie Data. Ein fragmentierter Index ist größer und langsamer – mehr Disk-I/O, mehr RAM-Usage, langsamere Queries.
Index-Fragmentierung-Detection:
const stats = db.logs.stats()
stats.indexSizes // Object mit Index-Names und Sizes
// Beispiel:
// {
// _id_: 50000000,
// userId_1: 80000000,
// timestamp_1: 60000000
// }
// Compare mit Data-Size
print(`Data Size: ${stats.size / (1024**2)} MB`)
print(`Index Size: ${stats.totalIndexSize / (1024**2)} MB`)
print(`Index-to-Data-Ratio: ${(stats.totalIndexSize / stats.size).toFixed(2)}`)Ein Index-to-Data-Ratio > 1.0 ist nicht ungewöhnlich (Indexes können größer sein als Data), aber wenn er deutlich wächst über Zeit, deutet das auf Fragmentierung.
Index-Rebuild:
// Drop und Recreate
db.logs.dropIndex("userId_1")
db.logs.createIndex({ userId: 1 })
// Oder: reIndex (rebuilds alle Indexes)
db.logs.reIndex()reIndex() rebuilds alle Indexes auf der Collection. Dies
ist schneller als einzelne Drop/Create für viele Indexes.
Background-Index-Builds:
MongoDB 4.2+ builded Indexes im Background – die Collection bleibt
accessible während Build. Pre-4.2 musste man
background: true spezifizieren, sonst blocked der
Build.
// Pre-4.2: Explicit background
db.logs.createIndex({ userId: 1 }, { background: true })
// 4.2+: Always background (Parameter ignored)
db.logs.createIndex({ userId: 1 })Rolling-Index-Rebuild in Replica-Sets:
Für Production sollte man Indexes Rolling rebuilden:
# 1. Rebuild auf Secondary 1
mongosh secondary1:27017 --eval "db.logs.reIndex()"
# 2. Rebuild auf Secondary 2
mongosh secondary2:27017 --eval "db.logs.reIndex()"
# 3. Step-Down Primary, rebuild old PrimaryCaveat: Index-Rebuild ist I/O-intensive:
Ein Index-Rebuild auf einer 100 GB Collection kann hunderte GB Disk-Writes generieren. Dies kann Production-Performance beeinträchtigen. Best-Practice: Schedule Index-Rebuilds während Low-Traffic-Periods.
In Sharded-Clusters können Chunks unequal über Shards distributed sein – ein Shard hat 70% der Data, andere haben 10% each. Dies führt zu Hotspots – der Heavy-Shard ist overloaded, andere sind underutilized.
Balancer-Basics:
MongoDB’s Balancer ist ein Background-Process, der kontinuierlich Chunks zwischen Shards moved, um Balance zu erhalten:
sh.status()
// Output zeigt Distribution:
// Shard 1: 500 chunks
// Shard 2: 300 chunks
// Shard 3: 200 chunks
// Imbalance detectedDer Balancer würde automatisch Chunks von Shard 1 zu Shard 2 und 3 moven.
Balancer-Schedule:
Per Default läuft der Balancer 24/7. Für Production kann man ihn auf Maintenance-Windows restricten:
// Balancer nur zwischen 2 AM und 6 AM
db.settings.updateOne(
{ _id: "balancer" },
{
$set: {
activeWindow: {
start: "02:00",
stop: "06:00"
}
}
},
{ upsert: true }
)Manual-Chunk-Moves:
Für sofortige Rebalancing kann man Chunks manuell moven:
// Move Chunk von Shard 1 zu Shard 2
sh.moveChunk(
"mydb.logs",
{ userId: "USER-12345" }, // Chunk-Range
"shard2"
)MongoDB bewegt den Chunk – kopiert Daten zu Destination-Shard, validiert, und updated Metadata.
Chunk-Size-Tuning:
Default-Chunk-Size ist 64 MB. Für große Clusters kann man größere Chunks nutzen (weniger Chunks = weniger Balancing-Overhead):
// Set Chunk-Size zu 128 MB
db.settings.updateOne(
{ _id: "chunksize" },
{ $set: { value: 128 } },
{ upsert: true }
)Balancer-Impact:
Chunk-Moves sind I/O-intensive – sie kopieren Daten über Network. Ein 64 MB Chunk-Move kann Sekunden bis Minuten dauern. Während des Moves ist der Chunk still accessible (Split-Brain-Prevention), aber Performance kann degradieren.
Monitoring-Balancer-Activity:
// Check if Balancer is Running
sh.isBalancerRunning()
// View recent Balancer-Actions
db.getSiblingDB("config").changelog.find({ what: "moveChunk.start" }).sort({ time: -1 }).limit(10)Disable-Balancer vor Maintenance:
Vor großen Maintenance-Operations (etwa Backups) sollte man den Balancer stoppen, um konsistente Snapshots zu garantieren:
sh.stopBalancer()
// Perform Maintenance
sh.startBalancer()Für Compliance oder Performance kann man Data geografisch organisieren via Zoned-Sharding:
// Tag Shards mit Zones
sh.addShardToZone("shard-eu", "EU")
sh.addShardToZone("shard-us", "US")
// Define Ranges für Zones
sh.updateZoneKeyRange(
"mydb.users",
{ country: "DE" },
{ country: "GR" }, // DE bis FR (alphabetisch)
"EU"
)
sh.updateZoneKeyRange(
"mydb.users",
{ country: "US" },
{ country: "US" },
"US"
)Der Balancer respektiert Zones – EU-Data bleibt auf EU-Shards, US-Data auf US-Shards.
Use-Case:
Für Data mit definierter Lifetime (Sessions, Logs, Cache) sind TTL-Indexes automatische Garbage-Collection:
// Logs expire nach 30 Tagen
db.logs.createIndex(
{ timestamp: 1 },
{ expireAfterSeconds: 30 * 24 * 60 * 60 }
)MongoDB’s Background-Task deleted automatisch Dokumente, wo
timestamp + expireAfterSeconds < now.
Deletion-Frequency:
TTL-Deletes laufen alle 60 Sekunden. Dies bedeutet: Ein Dokument expired nicht exact bei timestamp + 30 days, sondern bis zu 60 Sekunden später.
TTL-Index-Limitation:
TTL-Indexes funktionieren nur auf Date-Fields. Für non-Date-Expiration (etwa “delete nach 1000 accesses”) braucht man Custom-Logic.
TTL-Performance-Impact:
TTL-Deletes sind batch-wise – MongoDB deleted bis zu 100k Dokumente per Batch. Bei sehr hohen Expiration-Rates (Millionen Dokumente per Day) kann dies I/O-Impact haben.
Monitoring TTL-Activity:
db.serverStatus().metrics.ttl
// Output:
// {
// deletedDocuments: 1500000,
// passes: 25000
// }deletedDocuments zeigt Total-Deletes seit Server-Start,
passes zeigt wie oft der TTL-Monitor ran.
Database-Maintenance sollte scheduled sein, nicht ad-hoc:
Monthly-Maintenance:
// Fragmentierung-Check
const collections = db.getCollectionNames()
collections.forEach(coll => {
const stats = db[coll].stats()
const overhead = ((stats.storageSize - stats.size) / stats.storageSize * 100).toFixed(2)
if (overhead > 30) {
print(`${coll}: ${overhead}% overhead - COMPACT RECOMMENDED`)
}
})Quarterly-Index-Review:
// Unused-Indexes identifizieren
db.getCollectionNames().forEach(coll => {
const indexStats = db[coll].aggregate([{ $indexStats: {} }]).toArray()
indexStats.forEach(idx => {
if (idx.accesses.ops < 1000) {
print(`${coll}.${idx.name}: ${idx.accesses.ops} ops - DROP CANDIDATE`)
}
})
})Pre-Peak-Season-Optimization:
Vor High-Traffic-Periods (etwa Black-Friday für E-Commerce) sollte man: 1. Compact fragmentierte Collections 2. Rebuild Indexes 3. Balance Sharded-Clusters 4. Validate Backups
Dies garantiert optimal-Performance während Peak-Load.
Die folgende Tabelle fasst Maintenance-Operations zusammen:
| Operation | Impact | Duration | Downtime | Best-For |
|---|---|---|---|---|
compact |
Collection-Level | Minutes-Hours | Yes (Rolling: No) | Moderate Fragmentierung |
repairDatabase |
Database-Level | Hours-Days | Yes | Disaster-Recovery |
| Export-Reimport | Collection-Level | Hours-Days | Rolling: No | Extreme Fragmentierung |
| Index-Rebuild | Index-Level | Minutes-Hours | No (Background) | Index-Fragmentierung |
| Shard-Balancing | Cluster-Level | Continuous | No | Unbalanced-Clusters |
| TTL-Indexes | Automated | Continuous | No | Time-Based-Expiration |
Database-Maintenance ist nicht einmalig – “Reorganize und fertig” – sondern ongoing. Databases degenerieren kontinuierlich, Maintenance muss kontinuierlich sein. Die Best-Practice ist scheduled, periodic Maintenance – monatliche Fragmentierung-Checks, quarterly Index-Reviews, pre-peak-season Optimizations. Mit systematischem Maintenance bleibt Performance stabil über Zeit – nicht degrading graduell, sondern maintained proactively. Die Alternative – reaktives Maintenance nur wenn Performance-Problems obvious sind – führt zu Fire-Drills und Emergency-Downtime. Mit proaktivem Approach wird Maintenance von Panic-Response zu Routine-Operation – scheduled, tested, predictable.