Eine Query ohne Index ist ein Full-Collection-Scan – MongoDB muss jedes Dokument öffnen, evaluieren und entscheiden, ob es matched. Bei 100 Dokumenten ist dies trivial. Bei 1 Million Dokumenten dauert es Sekunden. Bei 100 Millionen ist es praktisch unbenutzbar. Ein Index ändert alles – dieselbe Query, die Sekunden dauerte, completed in Millisekunden. Dies ist nicht inkrementelle Verbesserung, sondern transformativ. Indexes sind der Unterschied zwischen einer langsamen, frustrierenden Datenbank und einer schnellen, responsiven.
Aber Indexes sind nicht kostenlos. Sie konsumieren Storage – bei großen Collections Gigabytes. Sie verlangsamen Writes – jedes Insert/Update muss alle Indexes aktualisieren. Sie erfordern RAM – MongoDB cached hot Indexes in Memory. Für Production-Systeme ist Index-Design eine der kritischsten Performance-Entscheidungen. Zu wenige Indexes → langsame Queries. Zu viele Indexes → langsame Writes, excessive RAM-Usage. Die Kunst ist, genau die richtigen Indexes zu haben – nicht mehr, nicht weniger.
Dieses Kapitel behandelt Indexierung systematisch – von Basics (wie Indexes funktionieren) über Index-Typen (Single-Field, Compound, Text, Geospatial) bis zu fortgeschrittenen Strategien (Covered-Queries, Index-Intersection, Partial-Indexes). Der Fokus ist auf praktischem Index-Design für Production-Workloads.
MongoDB nutzt B-Tree-Indexes (genauer: B+ Trees) – eine Datenstruktur, die schnelle Lookups, Range-Queries und Sorted-Access erlaubt. Ein B-Tree ist ein Self-Balancing-Tree, wo jeder Node multiple Keys und Pointers hat.
Ohne Index:
// Query ohne Index
db.users.find({ email: "alice@example.com" })
// MongoDB muss:
// 1. Jedes Dokument in users lesen
// 2. email-Field extrahieren
// 3. Mit "alice@example.com" vergleichen
// 4. Matching Dokumente returnieren
// Bei 1M Dokumenten: ~1M Checks, mehrere SekundenMit Index:
// Index erstellen
db.users.createIndex({ email: 1 })
// Selbe Query
db.users.find({ email: "alice@example.com" })
// MongoDB nutzt jetzt:
// 1. Index-Lookup: O(log N) statt O(N)
// 2. Direkt zu Dokument springen
// Bei 1M Dokumenten: ~20 Checks (log₂ 1M ≈ 20), MillisekundenDer Index ist eine separate Datenstruktur – ein sorted B-Tree von
email → _id-Mappings. MongoDB traversiert den Tree, findet
“alice@example.com” schnell (logarithmisch statt linear), und nutzt die
_id um das Dokument zu fetchen.
Index-Overhead:
Indexes sind nicht magisch – sie haben Costs:
Die Frage ist nicht “sollte ich Indexes haben” (ja, definitiv), sondern “welche Indexes brauche ich wirklich”.
Die einfachste Index-Form ist ein Single-Field-Index:
db.products.createIndex({ category: 1 })Das 1 bedeutet ascending-Order. -1 wäre
descending. Für Single-Field-Indexes ist die Direction meist irrelevant
(MongoDB kann den Index in beide Directions traversieren).
Use-Cases:
{ category: "Electronics" }{ price: { $gt: 100, $lt: 500 } }.sort({ price: 1 })// Profitiert von Index auf price
db.products.find({ price: { $gte: 100, $lte: 500 } }).sort({ price: 1 })**Index auf _id:**
MongoDB erstellt automatisch einen Unique-Index auf _id.
Dies ist der einzige Index, der immer existiert. Man kann ihn nicht
droppen (außer man droppt die Collection).
Unique-Indexes:
Für Uniqueness-Constraints:
db.users.createIndex({ email: 1 }, { unique: true })
// Duplicate insert fails
db.users.insertOne({ email: "alice@example.com" }) // OK
db.users.insertOne({ email: "alice@example.com" }) // Error: E11000Unique-Indexes sind nicht nur Performance-Tool, sondern auch Constraint-Enforcement.
Für Queries, die multiple Felder filtern oder sortieren, sind Compound-Indexes essentiell.
db.orders.createIndex({ customerId: 1, orderDate: -1 })
// Nutzt Index optimal
db.orders.find({ customerId: "CUST-123" }).sort({ orderDate: -1 })Index-Prefix-Rule:
Ein Compound-Index { a: 1, b: 1, c: 1 } kann folgende
Queries supporten:
{ a: X }{ a: X, b: Y }{ a: X, b: Y, c: Z }Aber NICHT:
{ b: Y } (fehlendes Prefix){ c: Z } (fehlendes Prefix)Dies ist die “Left-Prefix-Rule” – der Index ist nur nutzbar wenn die leftmost Felder im Query sind.
Beispiel:
db.users.createIndex({ country: 1, city: 1, age: 1 })
// Nutzt Index
db.users.find({ country: "US", city: "New York" })
// Nutzt Index NICHT effizient (missing country)
db.users.find({ city: "New York", age: 25 })Für die zweite Query bräuchte man einen separaten Index auf
{ city: 1, age: 1 }.
Equality, Range, Sort (ESR) Rule:
Für Compound-Indexes: Equality-Felder zuerst, dann Range-Felder, dann Sort-Felder.
// Query: status = "active", amount > 100, sort by createdAt
db.orders.find({
status: "active",
amount: { $gt: 100 }
}).sort({ createdAt: -1 })
// Optimaler Index: ESR-Rule
db.orders.createIndex({
status: 1, // E: Equality
amount: 1, // R: Range
createdAt: -1 // S: Sort
})Wenn man die Order vertauscht (etwa amount vor
status), ist der Index weniger effizient – MongoDB kann
nicht optimal filtern.
Direction-Matters für Compound-Indexes:
Bei Compound-Indexes mit Sorts ist Direction wichtig:
// Index
db.products.createIndex({ category: 1, price: -1 })
// Nutzt Index optimal
db.products.find({ category: "Electronics" }).sort({ price: -1 })
// Nutzt Index SUB-optimal (reverse direction)
db.products.find({ category: "Electronics" }).sort({ price: 1 })MongoDB kann den Index in reverse traversieren, aber es ist weniger effizient. Für Production sollte die Index-Direction dem Sort matchen.
Eine Covered-Query ist eine Query, wo alle returned Felder im Index sind – MongoDB muss das Dokument nicht fetchen.
// Index
db.users.createIndex({ username: 1, email: 1 })
// Covered Query
db.users.find(
{ username: "alice" },
{ username: 1, email: 1, _id: 0 } // Projection: nur indexierte Felder
)MongoDB kann diese Query komplett aus dem Index beantworten – kein Dokument-Access nötig. Dies ist die schnellste mögliche Query.
**Warum _id: 0?**
Per Default returned MongoDB _id. Wenn _id
nicht im Index ist, muss MongoDB das Dokument fetchen. Durch
_id: 0 suppressed man dies, erlaubt Covered-Query.
Explain zeigt Covered Queries:
db.users.find(
{ username: "alice" },
{ username: 1, email: 1, _id: 0 }
).explain("executionStats")
// Output enthält:
// totalDocsExamined: 0 <- Kein Dokument accessed!
// totalKeysExamined: 1 <- Nur IndextotalDocsExamined: 0 ist das Signature eines
Covered-Query.
Sparse-Indexes:
Per Default indexiert MongoDB auch Dokumente, wo das Field fehlt (als
null). Sparse-Indexes überspringen diese:
db.users.createIndex({ phoneNumber: 1 }, { sparse: true })Dies spart Storage – wenn nur 10% der Users ein Phone-Number haben, ist der Index 90% kleiner.
Caveat:
Sparse-Indexes können zu unerwarteten Results führen:
// Sparse Index
db.users.createIndex({ phoneNumber: 1 }, { sparse: true })
// Query für missing phone numbers
db.users.find({ phoneNumber: { $exists: false } })
// Nutzt NICHT den Index (weil der Index diese Dokumente nicht hat)Partial-Indexes:
Mächtiger als Sparse – man kann beliebige Conditions definieren:
db.orders.createIndex(
{ orderDate: 1 },
{
partialFilterExpression: {
status: "completed",
amount: { $gte: 100 }
}
}
)Dieser Index enthält nur completed Orders >= $100. Für Queries auf diese Subset ist der Index viel kleiner und effizienter.
Use-Case:
Für Collections, wo 90% der Dokumente “archived” sind und Queries fast immer auf “active” filtern:
db.documents.createIndex(
{ createdAt: -1 },
{ partialFilterExpression: { status: "active" } }
)Der Index ist 10x kleiner (nur active Dokumente), Queries auf active sind 10x schneller.
Für Full-Text-Search auf String-Feldern:
db.articles.createIndex({ content: "text" })
// Search
db.articles.find({ $text: { $search: "mongodb indexing" } })Text-Indexes tokenizen Strings, removen Stopwords, und supporten Ranking:
db.articles.find(
{ $text: { $search: "mongodb performance" } },
{ score: { $meta: "textScore" } }
).sort({ score: { $meta: "textScore" } })Dies returned Articles sortiert nach Relevanz (wie viele Query-Terms matchen).
Text-Index-Limitations:
Multi-Field-Text-Indexes:
db.articles.createIndex({
title: "text",
content: "text",
tags: "text"
})Die Search durchsucht alle drei Felder.
Use-Case:
E-Commerce-Product-Search, Blog-Content-Search, Document-Management-Systems.
Alternative:
Für komplexere Text-Search (Faceting, Fuzzy-Search, Autocomplete) sollte man dedicated Search-Engines wie Elasticsearch nutzen. MongoDB’s Text-Indexes sind für Basic-Full-Text ausreichend, aber nicht für Advanced-Search-Features.
Für geografische Daten (Coordinates) gibt es spezielle Indexes:
db.restaurants.createIndex({ location: "2dsphere" })
// Find restaurants within 5km
db.restaurants.find({
location: {
$near: {
$geometry: {
type: "Point",
coordinates: [13.405, 52.520] // Berlin
},
$maxDistance: 5000 // Meters
}
}
})Der 2dsphere-Index unterstützt Queries wie: -
$near: Find nearest - $geoWithin: Inside
Polygon/Circle - $geoIntersects: Intersecting with
Shape
GeoJSON-Format:
Location-Data muss GeoJSON-Format haben:
{
location: {
type: "Point",
coordinates: [longitude, latitude] // Wichtig: lon first!
}
}Use-Case:
Store-Locator, Delivery-Radius-Queries, Geo-Fencing.
Für temporäre Daten (Sessions, Logs, Cache):
db.sessions.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 3600 } // 1 hour
)
db.sessions.insertOne({
userId: "USER-123",
createdAt: new Date()
})MongoDB’s Background-Task deleted automatisch Dokumente, wo
createdAt + 3600 seconds < now. Dies läuft alle 60
Sekunden.
Caveats:
Use-Case:
Session-Management, Temporary-Token-Storage, Log-Retention-Policies.
Hashed-Indexes hashen den Field-Value vor Indexing:
db.users.createIndex({ userId: "hashed" })Primary-Use-Case:
Shard-Keys. Hashed-Shard-Keys distributed Daten gleichmäßig, auch wenn die Original-Values sequential sind.
Limitation:
Hashed-Indexes unterstützen nur Equality-Queries, keine Range-Queries:
// Funktioniert
db.users.find({ userId: "USER-123" })
// Funktioniert NICHT
db.users.find({ userId: { $gt: "USER-100" } })Die explain()-Method ist essentiell für
Index-Tuning:
db.users.find({ email: "alice@example.com" }).explain("executionStats")Wichtige Output-Felder:
{
executionStats: {
executionTimeMillis: 5,
totalKeysExamined: 1,
totalDocsExamined: 1,
executionStages: {
stage: "IXSCAN", // Index-Scan (gut)
indexName: "email_1"
}
}
}Key-Metrics:
| Metric | Bedeutung | Ideal |
|---|---|---|
executionTimeMillis |
Query-Duration | < 100ms |
totalKeysExamined |
Index-Keys gescannt | Nah an returned Docs |
totalDocsExamined |
Dokumente gescannt | Gleich returned Docs (Covered Query: 0) |
stage |
Execution-Strategy | IXSCAN (nicht COLLSCAN) |
Bad Signs:
stage: "COLLSCAN" → Kein Index genutzt!totalDocsExamined >> returned Docs →
Ineffizienter IndexexecutionTimeMillis > 1000 → Sehr langsamExample-Diagnosis:
// Query
db.orders.find({
status: "pending",
amount: { $gte: 100 }
}).sort({ createdAt: -1 })
// Explain zeigt:
// stage: COLLSCAN
// totalDocsExamined: 1000000
// Lösung: Index erstellen
db.orders.createIndex({ status: 1, amount: 1, createdAt: -1 })
// Nach Index:
// stage: IXSCAN
// totalDocsExamined: 523 (nur relevante)Wenn multiple Indexes existieren, die eine Query supporten könnten, wählt MongoDB’s Query-Planner den “besten”:
db.orders.createIndex({ status: 1 })
db.orders.createIndex({ customerId: 1 })
db.orders.createIndex({ status: 1, customerId: 1 })
// Query
db.orders.find({ status: "pending", customerId: "CUST-123" })MongoDB evaluiert alle drei Indexes, führt Trial-Runs durch, und cached den Winner. Dies passiert automatisch.
Index-Hints (Override):
Man kann den Planner overriden:
db.orders.find({
status: "pending",
customerId: "CUST-123"
}).hint({ status: 1, customerId: 1 })Dies forced den Compound-Index. Hints sollten nur genutzt werden wenn man sicher ist, dass der Planner falsch wählt (rare).
List Indexes:
db.users.getIndexes()Output zeigt alle Indexes mit Names, Keys, Options.
Drop Index:
// By Name
db.users.dropIndex("email_1")
// By Key-Pattern
db.users.dropIndex({ email: 1 })Rebuild Indexes:
Bei Index-Corruption oder nach Major-MongoDB-Upgrades:
db.users.reIndex()Dies droppt und recreated alle Indexes. WARNING: Dies locked die Collection während Rebuild – für Production nur in Maintenance-Windows.
Bei Index-Creation kann man Background-Mode wählen:
db.users.createIndex({ email: 1 }, { background: true })Foreground (Default): - Schneller Build - Aber: Collection ist locked (keine Reads/Writes während Build)
Background: - Langsamer Build - Aber: Collection bleibt accessible
Für Production immer background: true, außer während
Maintenance-Windows.
Hinweis:
MongoDB 4.2+ baut alle Indexes im Background (der Parameter ist deprecated). Dies war ein großer Usability-Win – keine versehentlichen Production-Lockouts mehr.
MongoDB-Limits:
Best Practices:
Nur nötige Indexes: Start mit keinen Indexes
(außer _id), füge hinzu basierend auf
Slow-Query-Logs.
Monitor Index-Usage:
db.users.aggregate([{ $indexStats: {} }])Dies zeigt Usage-Stats – welche Indexes wie oft genutzt werden. Unused Indexes sollten dropped werden.
ESR-Rule für Compound-Indexes: Equality, Range, Sort.
Covered-Queries wo möglich: Projection auf nur indexierte Felder.
Partial-Indexes für Subset-Queries: Spart Storage und RAM.
Index-Size im RAM: Prüfe
db.collection.stats().indexSizes. Wenn Indexes nicht in RAM
passen, wird Performance leiden.
Avoid Index-Bloat: Bei Updates, die Indexed-Fields ändern, kann es zu Index-Bloat kommen. Periodisch reIndex() in Maintenance-Windows.
Die folgende Tabelle fasst Index-Typen zusammen:
| Index-Type | Use-Case | Supports | Limitations |
|---|---|---|---|
| Single-Field | Simple Queries | Equality, Range, Sort | - |
| Compound | Multi-Field-Queries | Complex Filters + Sort | Left-Prefix-Rule |
| Unique | Uniqueness-Constraint | Same as Single/Compound | - |
| Text | Full-Text-Search | Tokenized-Search, Ranking | One per Collection (legacy) |
| Geospatial | Location-Queries | $near, $geoWithin | GeoJSON-Format required |
| Hashed | Even-Distribution (Sharding) | Equality only | No Range-Queries |
| TTL | Auto-Expiration | Date-Based-Deletion | 60s-Delay |
| Partial | Subset-Indexing | Condition-Based | Query muss Condition matchen |
| Sparse | Skip-Nulls | Memory-Saving | Unexpected Results bei null-Queries |
Indexierung ist die wichtigste Performance-Optimierung für MongoDB.
Ein gut-designtes Index-Set kann eine Datenbank von unusably-slow zu
production-ready transformieren. Aber Indexes sind nicht “mehr ist
besser” – jeder Index kostet Storage, RAM und Write-Performance. Die
Kunst ist, exactly die richtigen Indexes zu haben. Der Process: Start
minimal (nur _id), enable Slow-Query-Logs, monitor welche
Queries slow sind, erstelle Indexes für diese Queries, validate mit
Explain, iterate. Mit systematischem Index-Design und regelmäßigem
Monitoring wird MongoDB von decent zu excellent Performance – capable of
sub-100ms Queries auf Millionen-Dokument-Collections.