Update-Operationen sind fundamentaler Teil jeder Datenbank-Anwendung. User ändert sein Passwort, ein Product läuft aus dem Stock, ein Counter wird inkrementiert, ein Tag wird zu einer Liste hinzugefügt – all dies sind Updates. Im Gegensatz zu relationalen Datenbanken, wo Updates typischerweise komplette Rows überschreiben, erlaubt MongoDB granulare, Feld-Level-Modifikationen. Man kann ein einzelnes nested Field ändern, ein Array-Element hinzufügen oder einen numerischen Wert inkrementieren – alles ohne das gesamte Dokument zu re-schreiben.
Diese Granularität ist nicht nur Convenience, sondern
Performance-kritisch. Ein 10-KB-Dokument mit einem 1-Byte-Counter zu
überschreiben, nur um den Counter zu inkrementieren, ist
verschwenderisch. MongoDB’s Update-Operatoren erlauben atomare, in-place
Modifications. Aber diese Macht kommt mit Komplexität – dutzende
Operatoren mit subtilen Semantiken, Array-Update-Patterns mit
$-Positional-Operatoren, Concurrency-Issues bei
konkurrierenden Updates. Dieses Kapitel navigiert durch die
Update-Landschaft systematisch, mit Fokus auf praktische Patterns und
häufige Pitfalls.
MongoDB bietet zwei primäre Update-Methoden, die sich darin unterscheiden, wie viele Dokumente sie betreffen.
updateOne() – Modifiziert das erste Match:
db.users.updateOne(
{ username: "alice" }, // Filter
{ $set: { lastLogin: new Date() } } // Update
)Dies findet das erste Dokument mit username: "alice" und
updated es. Wenn multiple Dokumente matchen, wird nur das erste (gemäß
natural order oder Index-Order) updated. Die Response zeigt, was
passiert ist:
{
acknowledged: true,
matchedCount: 1, // 1 Dokument matched den Filter
modifiedCount: 1, // 1 Dokument wurde modified
upsertedId: null // Kein Upsert passiert
}matchedCount vs. modifiedCount ist eine
subtile aber wichtige Unterscheidung. Wenn das Update nichts ändert
(etwa $set auf einen Wert, den das Feld bereits hat), ist
matchedCount: 1 aber modifiedCount: 0. Dies
kann relevant sein für Idempotenz-Checks.
updateMany() – Modifiziert alle Matches:
db.users.updateMany(
{ status: "inactive" },
{ $set: { archived: true, archivedAt: new Date() } }
)Dies findet alle Dokumente mit status: "inactive" und
updated sie alle. Die Response zeigt totale Counts:
{
acknowledged: true,
matchedCount: 1532,
modifiedCount: 1532
}Für Bulk-Operations – etwa “Archive all inactive users” oder “Set
default value for all documents missing a field” – ist
updateMany() essentiell. Aber Vorsicht: Ein falsch
geschriebener Filter kann unbeabsichtigt tausende Dokumente updaten.
Safety-Pattern – Dry-Run via countDocuments:
Bevor man ein großes updateMany() ausführt, sollte man
prüfen, wie viele Dokumente betroffen sind:
// Check wie viele matched werden
const affectedCount = db.users.countDocuments({ status: "inactive" })
print(`Would update ${affectedCount} documents`)
// User-Confirmation in Script
if (affectedCount > 1000) {
print("WARNING: Large update! Confirm before proceeding.")
}
// Dann actual update
db.users.updateMany({ status: "inactive" }, { $set: { archived: true } })Dies verhindert versehentliche Mass-Updates bei Typos im Filter.
Die fundamentalsten Update-Operatoren sind $set (Feld
setzen oder überschreiben) und $unset (Feld entfernen).
$set – Feld setzen oder erstellen:
db.users.updateOne(
{ username: "alice" },
{ $set: {
email: "alice@newdomain.com",
updatedAt: new Date()
}}
)Wenn email bereits existiert, wird es überschrieben.
Wenn nicht, wird es erstellt. $set ist idempotent –
mehrmaliges Ausführen mit demselben Wert ändert nichts (außer
modifiedCount bleibt 0 nach dem ersten).
$set auf nested Fields:
db.users.updateOne(
{ username: "alice" },
{ $set: { "profile.bio": "Coffee lover and developer" } }
)Dot-Notation erlaubt Updates auf tief verschachtelte Felder ohne das
gesamte Parent-Object zu überschreiben. Wenn profile nicht
existiert, wird es erstellt. Wenn profile existiert aber
bio nicht, wird bio hinzugefügt.
$unset – Feld komplett entfernen:
db.users.updateOne(
{ username: "alice" },
{ $unset: { temporaryToken: "" } }
)Der Value zu $unset ist ignoriert (oft ""
oder 1 per Convention) – es geht nur um das Key. Das Feld
wird komplett aus dem Dokument entfernt, nicht auf null
gesetzt. Dies ist wichtig für Storage-Efficiency und Schema-Clarity.
$set vs. $unset vs. null:
Drei Wege, ein “kein Wert”-Zustand zu repräsentieren:
// Option 1: Field existiert mit null
{ username: "alice", nickname: null }
// Option 2: Field fehlt komplett
{ username: "alice" }
// Option 3: Field existiert mit empty string
{ username: "alice", nickname: "" }Welche wählen? Es hängt ab. null ist explizit – “wir
wissen, dass es keinen Nickname gibt”. Fehlendes Field kann bedeuten
“wir haben nie gefragt” oder “Schema-Version vor diesem Field”. Empty
string ist für Strings eine Option, aber semantisch anders als null. Die
Wahl beeinflusst Queries – { nickname: null } matched
sowohl null als auch fehlendes Field, was oft nicht
gewünscht ist.
Für Counter, Scores, Quantities – numerische Felder, die inkrementiert oder modifiziert werden – gibt es spezielle Operatoren.
$inc – Inkrementieren oder Dekrementieren:
// Login-Counter erhöhen
db.users.updateOne(
{ username: "alice" },
{ $inc: { loginCount: 1 } }
)
// Score um 10 erhöhen
db.players.updateOne(
{ playerId: "P123" },
{ $inc: { score: 10 } }
)
// Quantity reduzieren (negatives Inkrement)
db.inventory.updateOne(
{ productId: "PROD-A" },
{ $inc: { quantity: -5 } }
)$inc ist atomar – bei konkurrierenden Updates gehen
keine Inkremente verloren. Ohne $inc müsste man das
Dokument fetchen, den Wert inkrementieren, und zurückschreiben – ein
Race-Condition-Alptraum. $inc ist die richtige Lösung für
alle Counter-Szenarien.
Wenn das Feld nicht existiert, wird es mit dem Inkrement-Value initialisiert:
// loginCount existiert nicht -> wird auf 1 gesetzt
db.users.updateOne(
{ username: "newuser" },
{ $inc: { loginCount: 1 } }
)$mul – Multiplizieren:
// Preise um 10% erhöhen (multiply by 1.1)
db.products.updateMany(
{ category: "electronics" },
{ $mul: { price: 1.1 } }
)$mul multipliziert den bestehenden Wert. Für
Prozentsatz-basierte Änderungen ist dies praktischer als manuelles
Calculate-and-Set.
Arrays sind ein zentraler Teil von MongoDB’s Document-Model. Die Array-Update-Operatoren sind mächtig aber komplex.
$push – Element zu Array hinzufügen:
// Tag zu Array hinzufügen
db.posts.updateOne(
{ postId: "POST-123" },
{ $push: { tags: "mongodb" } }
)Dies appendet “mongodb” zum Ende des tags-Arrays. Wenn
tags nicht existiert, wird es als Array erstellt. Wenn
tags kein Array ist (etwa ein String), wirft MongoDB einen
Error.
$push mit $each für multiple Elements:
db.posts.updateOne(
{ postId: "POST-123" },
{ $push: { tags: { $each: ["database", "nosql", "tutorial"] } } }
)Die $each-Syntax pusht multiple Elements auf einmal.
Ohne $each würde MongoDB das gesamte Array als einzelnes
Element pushen (nested Array).
$push mit Modifiers für kontrollierten Insert:
// Push und sortiere Array, limitiere auf 10 Elemente
db.users.updateOne(
{ username: "alice" },
{ $push: {
recentSearches: {
$each: ["mongodb tutorial"],
$position: 0, // Insert at beginning
$slice: 10 // Keep only first 10
}
}}
)Dies implementiert ein “Recent Searches”-Feature – neue Searches
werden an den Anfang gepusht, das Array auf 10 limitiert (älteste werden
dropped). Die Modifiers: - $position: Wo im Array
inserieren (0 = Anfang) - $slice: Array-Length limitieren
(negativ = last N, positiv = first N) - $sort: Array nach
dem Push sortieren
$addToSet – Push nur wenn nicht bereits vorhanden:
db.users.updateOne(
{ username: "alice" },
{ $addToSet: { favorites: "PROD-123" } }
)Dies pusht “PROD-123” zu favorites nur wenn es nicht
bereits drin ist. Für Sets (Arrays ohne Duplikate) ist dies die richtige
Methode. Man kann $each kombinieren:
db.users.updateOne(
{ username: "alice" },
{ $addToSet: {
favorites: {
$each: ["PROD-123", "PROD-456", "PROD-789"]
}
}}
)Jedes Element wird nur hinzugefügt, wenn es noch nicht existiert.
$pull – Element aus Array entfernen:
// Entferne spezifisches Tag
db.posts.updateOne(
{ postId: "POST-123" },
{ $pull: { tags: "deprecated" } }
)Dies entfernt alle Vorkommen von “deprecated” aus tags.
Für Arrays of Objects kann man Query-Syntax nutzen:
// Entferne alle items mit quantity 0
db.orders.updateOne(
{ orderId: "ORD-123" },
{ $pull: {
items: { quantity: 0 }
}}
)$pop – Erstes oder letztes Element entfernen:
// Letztes Element entfernen
db.users.updateOne(
{ username: "alice" },
{ $pop: { recentSearches: 1 } }
)
// Erstes Element entfernen
db.users.updateOne(
{ username: "alice" },
{ $pop: { recentSearches: -1 } }
)1 ist last, -1 ist first. Nützlich für
Queue- oder Stack-ähnliche Strukturen.
Für Arrays of Objects ist es oft nötig, ein spezifisches Element zu
updaten basierend auf einer Condition. Die Positional-Operatoren
$, $[] und $[<identifier>]
erlauben dies.
$ – Update das erste matched Element:
// Update das item mit productId "PROD-A"
db.orders.updateOne(
{
orderId: "ORD-123",
"items.productId": "PROD-A" // Match condition
},
{ $set: { "items.$.quantity": 5 } } // $ referenziert matched element
)Das $ im Update-Teil referenziert das erste
Array-Element, das im Query-Filter matched hat. Dies funktioniert nur,
wenn der Filter ein Array-Element matched – sonst ist $
undefined.
$[] – Update alle Array-Elemente:
// Setze shipped: true für alle items
db.orders.updateOne(
{ orderId: "ORD-123" },
{ $set: { "items.$[].shipped": true } }
)Kein Filter nötig – $[] updated alle Elemente des
Arrays.
$[identifier] mit arrayFilters – Conditional Update:
// Update nur items mit quantity > 0
db.orders.updateOne(
{ orderId: "ORD-123" },
{ $set: { "items.$[elem].status": "available" } },
{ arrayFilters: [{ "elem.quantity": { $gt: 0 } }] }
)Der arrayFilters-Parameter definiert Conditions.
elem ist ein Placeholder, der im Update-Teil referenced
wird. Dies updated nur Array-Elements, die die Condition erfüllen. Sehr
mächtig für komplexe Array-Updates.
Beispiel mit mehreren Filters:
// Update items mit quantity > 0 UND price < 100
db.orders.updateOne(
{ orderId: "ORD-123" },
{ $set: { "items.$[elem].discount": 0.1 } },
{ arrayFilters: [
{ "elem.quantity": { $gt: 0 } },
{ "elem.price": { $lt: 100 } }
]}
)Statt Feld-Level-Updates kann man das gesamte Dokument ersetzen:
db.users.replaceOne(
{ username: "alice" },
{
username: "alice",
email: "alice@example.com",
profile: {
firstName: "Alice",
lastName: "Smith"
},
createdAt: new Date("2024-01-01")
}
)Dies überschreibt das komplette Dokument (außer _id, das
immutable ist). Alle Felder, die im Replacement nicht sind, werden
gelöscht. replaceOne() ist gefährlich – man kann
versehentlich Felder verlieren. Update-Operatoren ($set,
etc.) sind safer und expliziter.
Use-Case für replaceOne(): Wenn das Dokument fundamental
re-structured werden muss, etwa bei Schema-Migrations oder wenn ein
externes System ein komplettes neues Dokument liefert.
Upserts sind eine mächtige MongoDB-Feature – atomic “Insert-or-Update” ohne Race Conditions.
db.counters.updateOne(
{ name: "pageViews" },
{ $inc: { count: 1 } },
{ upsert: true }
)Beim ersten Call existiert kein Dokument mit
name: "pageViews". MongoDB inserted eines mit
name: "pageViews" und count: 1. Bei
nachfolgenden Calls existiert es bereits, also wird count
inkrementiert.
Die Response bei Upsert zeigt die inserted ID:
{
acknowledged: true,
matchedCount: 0,
modifiedCount: 0,
upsertedId: ObjectId('...')
}matchedCount: 0 weil das Dokument nicht existierte.
upsertedId zeigt die generierte _id des neuen
Dokuments.
$setOnInsert – Felder nur beim Insert setzen:
Manchmal will man Felder setzen, die nur bei Insert, nicht bei Update gesetzt werden:
db.users.updateOne(
{ username: "alice" },
{
$set: { lastSeen: new Date() },
$setOnInsert: {
createdAt: new Date(),
signupSource: "web"
}
},
{ upsert: true }
)Beim Insert werden lastSeen, createdAt und
signupSource gesetzt. Bei Update wird nur
lastSeen aktualisiert, die anderen bleiben unverändert.
Dies ist perfect für “Created/Updated Timestamps”-Patterns.
Manchmal braucht man das Dokument zurück nach dem Update – etwa um
die neue Version zu loggen oder an den Client zu senden.
findOneAndUpdate() returned das Dokument:
const updatedUser = db.users.findOneAndUpdate(
{ username: "alice" },
{ $inc: { loginCount: 1 } },
{ returnDocument: "after" } // Return updated version
)
print(updatedUser.loginCount) // Der neue WertDer returnDocument-Parameter kann “before” (Default)
oder “after” sein. “before” returned das Dokument vor dem Update,
“after” danach.
Use-Case: Optimistic Locking, Transaction-ähnliche Patterns, oder wenn man den updated Value sofort braucht ohne separates Read.
// Atomic Counter mit Return
const result = db.counters.findOneAndUpdate(
{ name: "orderNumber" },
{ $inc: { sequence: 1 } },
{ returnDocument: "after", upsert: true }
)
const newOrderNumber = result.sequence
// Nutze newOrderNumber für neuen OrderDies garantiert, dass jeder Order eine unique, sequential Number bekommt ohne Race Conditions.
MongoDB-Updates sind atomar auf Document-Level. Ein einzelnes Update (auch wenn es multiple Felder ändert) ist eine atomic Operation – entweder alle Änderungen passieren oder keine.
Race Conditions ohne atomare Operatoren:
// SCHLECHT: Read-Modify-Write Race Condition
const user = db.users.findOne({ username: "alice" })
user.loginCount += 1
db.users.updateOne(
{ username: "alice" },
{ $set: { loginCount: user.loginCount } }
)Wenn zwei Clients diese Sequenz concurrent ausführen: 1. Client A
reads loginCount: 10 2. Client B reads
loginCount: 10 3. Client A writes
loginCount: 11 4. Client B writes
loginCount: 11
Resultat: Zwei Logins, aber loginCount ist 11, nicht 12.
Ein Login ging verloren.
Richtig mit $inc:
db.users.updateOne(
{ username: "alice" },
{ $inc: { loginCount: 1 } }
)Dies ist atomar. Concurrent Updates werden serialized vom Server – beide Inkremente werden korrekt angewendet.
Optimistic Locking Pattern:
Für komplexere Updates, wo man Read-Modify-Write braucht, kann man eine Version-Number nutzen:
// Read mit Version
const user = db.users.findOne({ username: "alice" })
const currentVersion = user.version
// Modify
user.email = "newemail@example.com"
user.version += 1
// Write nur wenn Version unverändert
const result = db.users.updateOne(
{ username: "alice", version: currentVersion },
{ $set: {
email: user.email,
version: user.version
}}
)
if (result.matchedCount === 0) {
print("Conflict: Document was modified by another client")
// Retry or handle conflict
}Wenn ein anderer Client das Dokument zwischenzeitlich updated hat, ist die Version anders und das Update matcht nicht. Dies detektiert Conflicts und erlaubt Retry-Logik.
Updates nutzen Indexes für das Finden der zu updatendenden Dokumente, genauso wie Queries. Ein Update ohne passenden Index scannt die gesamte Collection:
// Langsam ohne Index auf email
db.users.updateOne(
{ email: "alice@example.com" },
{ $set: { lastSeen: new Date() } }
)
// -> COLLSCAN
// Mit Index
db.users.createIndex({ email: 1 })
// Jetzt: IXSCANAber Updates haben einen zusätzlichen Performance-Aspekt: Index-Maintenance. Jedes Update, das ein indiziertes Feld ändert, muss den Index updaten:
// Drei Indexes: email, status, createdAt
db.users.createIndex({ email: 1 })
db.users.createIndex({ status: 1 })
db.users.createIndex({ createdAt: 1 })
// Update ändert email -> muss email-Index updaten
db.users.updateOne(
{ username: "alice" },
{ $set: { email: "newemail@example.com" } }
)Viele Indexes machen Writes langsamer. Der Trade-off: Indexes beschleunigen Reads dramatisch, aber verlangsamen Writes. Für Read-Heavy-Workloads ist dies akzeptabel. Für Write-Heavy-Workloads sollte man Indexes minimieren.
Für Bulk-Operations ist bulkWrite() effizienter als Loop
mit einzelnen Updates:
const operations = [
{
updateOne: {
filter: { username: "alice" },
update: { $set: { status: "active" } }
}
},
{
updateOne: {
filter: { username: "bob" },
update: { $inc: { loginCount: 1 } }
}
},
{
updateMany: {
filter: { status: "inactive", lastSeen: { $lt: oldDate } },
update: { $set: { archived: true } }
}
}
]
db.users.bulkWrite(operations)Alle Operations werden in einem Batch executed – ein Netzwerk-Roundtrip statt dutzende. Performance-Gewinn ist signifikant bei großen Bulk-Updates.
Die folgende Tabelle fasst Update-Operatoren zusammen:
| Operator | Zweck | Atomicity | Use-Case |
|---|---|---|---|
$set |
Feld setzen/überschreiben | Atomic | Jegliche Field-Updates |
$unset |
Feld entfernen | Atomic | Schema-Cleanup, Privacy |
$inc |
Numerisch inkrementieren | Atomic | Counters, Scores |
$mul |
Numerisch multiplizieren | Atomic | Prozentsatz-Änderungen |
$push |
Element zu Array adden | Atomic | Tags, Logs, Listen |
$pull |
Element aus Array entfernen | Atomic | Cleanup, Removal |
$addToSet |
Add wenn nicht existent | Atomic | Sets, Unique-Listen |
$ |
Update matched Array-Element | Atomic | Conditional Array-Update |
$[] |
Update alle Array-Elemente | Atomic | Bulk-Array-Modify |
$[id] |
Update filtered Array-Elemente | Atomic | Complex Array-Updates |
Update-Operationen sind mächtiger und komplexer als simples
Überschreiben. Die granularen Operatoren erlauben effiziente, atomare
Modifications ohne Read-Modify-Write-Cycles. Aber sie erfordern
Verständnis – der falsche Operator kann zu subtilen Bugs führen.
$set statt $inc für Counter verliert Updates
bei Concurrency. $push ohne $each pusht nested
Arrays statt Elements. replaceOne() löscht versehentlich
Felder. Die Best Practice: Nutze den spezifischsten Operator für den
Use-Case, teste Concurrency-Scenarios, und überwache Performance via
Slow-Query-Logs. Mit korrektem Operator-Einsatz sind Updates nicht nur
funktional korrekt, sondern auch performant und robust gegen Race
Conditions.