Indexes blind zu erstellen ist wie im Dunkeln zu schießen – man
könnte treffen, aber wahrscheinlich nicht. Production-Systeme generieren
kontinuierlich Performance-Daten: Welche Queries laufen? Wie lange
dauern sie? Welche Indexes werden genutzt? Welche fehlen? Diese Daten
sind Gold für Performance-Tuning. MongoDB bietet mehrere Tools, um
Index-Performance zu messen, zu analysieren und zu optimieren:
explain() für Query-Analysis, $indexStats für
Index-Usage-Tracking, Profiler für Slow-Query-Detection, und
serverStatus() für System-Wide-Metrics.
Der systematische Approach zu Index-Optimization ist nicht “erstelle Indexes und hoffe”, sondern “messe, analysiere, optimize, validate”. Man startet mit Monitoring – welche Queries sind langsam? Dann diagnostiziert man – nutzen sie Indexes? Wenn nicht, warum? Dann optimiert man – erstelle missing Indexes, droppe unused Indexes. Dann validiert man – sind die Queries jetzt schneller? Dieser Cycle ist kontinuierlich – nicht einmal, sondern ongoing.
Dieses Kapitel durchläuft MongoDB’s Metrics-Tools systematisch, mit Fokus auf praktische Workflows für Production-Monitoring und Index-Optimization.
Die explain()-Method ist das primäre Tool für
Query-Analysis. Sie zeigt genau, wie MongoDB eine Query executed –
welcher Index genutzt wird, wie viele Dokumente gescannt werden, wie
lange es dauert.
db.orders.find({
status: "pending",
amount: { $gte: 100 }
}).explain("executionStats")Die Output ist umfangreich – dutzende Felder mit technischen Details. Die wichtigsten Metriken für Performance-Analysis:
{
executionStats: {
executionSuccess: true,
nReturned: 523, // Returned documents
executionTimeMillis: 5, // Total query time
totalKeysExamined: 523, // Index keys scanned
totalDocsExamined: 523, // Documents examined
executionStages: {
stage: "IXSCAN", // Execution strategy
indexName: "status_1_amount_1",
keysExamined: 523,
docsExamined: 523
}
}
}Kritische Metriken-Interpretation:
1. executionTimeMillis:
Die Total-Duration. Für Production-Queries ist das Ziel < 100ms für p95 (95th percentile). Queries > 1000ms sind problematisch und sollten investigated werden.
executionTimeMillis: 5 // Excellent
executionTimeMillis: 150 // Acceptable
executionTimeMillis: 2500 // Problem!2. stage: IXSCAN vs. COLLSCAN:
IXSCAN bedeutet Index-Scan – gut. COLLSCAN
bedeutet Collection-Scan – schlecht (außer für sehr kleine Collections
< 1000 Dokumente).
// Gut
executionStages: {
stage: "IXSCAN",
indexName: "email_1"
}
// Schlecht
executionStages: {
stage: "COLLSCAN",
direction: "forward"
}Ein COLLSCAN ist der klarste Indicator für missing Index. Die sofortige Action: Identifiziere das gefilterte Field und erstelle einen Index.
3. Scan-Efficiency: totalKeysExamined vs. nReturned:
Idealerweise sind diese Zahlen gleich – MongoDB scannt genau die Dokumente, die returned werden. Eine große Diskrepanz bedeutet ineffizienten Index:
// Efficient
nReturned: 100
totalKeysExamined: 102 // ~2% Overhead, sehr gut
totalDocsExamined: 100
// Inefficient
nReturned: 100
totalKeysExamined: 50000 // Scannt 50k, returned 100 – sehr ineffizient
totalDocsExamined: 50000Dies passiert wenn der Index nicht selective genug ist. Beispiel:
Index auf status, aber 99% der Dokumente haben
status: "active". Ein Query für
status: "active" scannt fast alle Dokumente trotz
Index.
4. totalDocsExamined: 0 → Covered Query:
Wenn totalDocsExamined: 0 aber
nReturned > 0, ist dies eine Covered-Query – die
schnellste mögliche Query. MongoDB beantwortet die Query komplett aus
dem Index ohne Dokument-Access.
nReturned: 100
totalKeysExamined: 100
totalDocsExamined: 0 // Covered Query!Dies ist nur möglich wenn alle returned Felder im Index sind und
_id excluded ist.
Explain-Modi:
explain() hat drei Modi:
// queryPlanner: Zeigt Execution-Plan, führt Query nicht aus
db.collection.find(...).explain("queryPlanner")
// executionStats: Führt Query aus, zeigt Execution-Stats
db.collection.find(...).explain("executionStats")
// allPlansExecution: Zeigt alle getesteten Plans (sehr verbose)
db.collection.find(...).explain("allPlansExecution")Für Performance-Analysis ist executionStats der richtige
Mode – er führt die Query tatsächlich aus und misst
Real-Performance.
Während explain() eine einzelne Query analysiert,
tracked $indexStats langfristige Index-Usage – welche
Indexes wie oft genutzt werden.
db.orders.aggregate([{ $indexStats: {} }])Output:
[
{
name: "status_1",
key: { status: 1 },
host: "mongodb-server:27017",
accesses: {
ops: 125432,
since: ISODate("2024-01-01T00:00:00Z")
}
},
{
name: "customerId_1_orderDate_-1",
key: { customerId: 1, orderDate: -1 },
accesses: {
ops: 89321,
since: ISODate("2024-01-01T00:00:00Z")
}
},
{
name: "legacyField_1",
key: { legacyField: 1 },
accesses: {
ops: 3,
since: ISODate("2024-01-01T00:00:00Z")
}
}
]Interpretation:
Index-Cleanup-Strategy:
Periodisch (etwa quarterly) sollte man $indexStats reviewen und unused Indexes droppen:
// Find indexes mit < 100 Operations in letzten 30 Tagen
db.orders.aggregate([
{ $indexStats: {} },
{ $match: { "accesses.ops": { $lt: 100 } } },
{ $project: { name: 1, "accesses.ops": 1 } }
])
// Für jeden: Prüfe ob er wirklich unused ist, dann drop
db.orders.dropIndex("legacyField_1")Caveat:
$indexStats resetted bei Server-Restart. Für
langfristiges Tracking sollte man die Stats periodisch in eine
Monitoring-Collection exportieren:
// Daily-Job: Export index stats
db.orders.aggregate([
{ $indexStats: {} },
{ $addFields: {
collectionName: "orders",
timestamp: new Date()
}},
{ $out: "index_usage_history" }
])Dies erlaubt historische Analysis – welche Indexes waren mal nützlich aber sind jetzt deprecated?
MongoDB’s Profiler logged alle Queries (oder nur langsame) in die
system.profile-Collection. Dies ist essentiell für
Production-Monitoring – welche Queries sind slow und warum?
Profiler aktivieren:
// Level 1: Nur Slow-Queries (> 100ms)
db.setProfilingLevel(1, { slowms: 100 })
// Level 2: Alle Queries (nur für kurze Debugging-Sessions!)
db.setProfilingLevel(2)
// Level 0: Profiler aus
db.setProfilingLevel(0)Level 1 ist für Production: Loggt nur Queries, die
länger als slowms dauern. Level 2 loggt alles – nützlich
für kurze Debug-Sessions, aber zu verbose für kontinuierliches
Monitoring (kann Performance selbst beeinträchtigen).
Profiler-Output analysieren:
// Neueste Slow-Queries
db.system.profile.find().sort({ ts: -1 }).limit(10)
// Queries ohne Index (COLLSCAN)
db.system.profile.find({
"command.filter": { $exists: true },
"planSummary": /COLLSCAN/
}).sort({ millis: -1 })
// Langsamste Queries
db.system.profile.find().sort({ millis: -1 }).limit(10)Example-Output:
{
op: "query",
ns: "mydb.orders",
command: {
find: "orders",
filter: { status: "pending", customerId: "CUST-123" }
},
keysExamined: 0,
docsExamined: 1500000,
nreturned: 10,
millis: 2350,
planSummary: "COLLSCAN",
ts: ISODate("2024-01-20T10:30:00Z")
}Diagnosis:
planSummary: "COLLSCAN" → Kein Index genutztdocsExamined: 1.5M vs nreturned: 10 → Sehr
ineffizientmillis: 2350 → 2.3 Sekunden – inakzeptabelAction:
// Create Index auf gefilterten Feldern
db.orders.createIndex({ status: 1, customerId: 1 })
// Validate mit explain
db.orders.find({ status: "pending", customerId: "CUST-123" }).explain("executionStats")
// Jetzt: IXSCAN, docsExamined: 10, millis: 5Profiler-Size-Management:
Die system.profile-Collection ist eine Capped-Collection
mit Default-Size 1MB. Bei High-Traffic kann dies schnell voll sein. Man
kann die Size konfigurieren:
// Recreate system.profile mit größerer Size
db.setProfilingLevel(0)
db.system.profile.drop()
db.createCollection("system.profile", {
capped: true,
size: 100000000 // 100 MB
})
db.setProfilingLevel(1, { slowms: 100 })Automated-Alerting:
Für Production sollte man Alerts auf Slow-Queries haben:
// Periodisch prüfen: Gibt es neue Slow-Queries?
const slowQueries = db.system.profile.find({
ts: { $gte: new Date(Date.now() - 3600000) }, // Last hour
millis: { $gte: 1000 } // > 1 second
}).toArray()
if (slowQueries.length > 10) {
// Alert Team
sendAlert("MongoDB Slow-Queries detected", slowQueries)
}Während explain() und Profiler Query-Level-Metrics
liefern, gibt serverStatus() System-Wide-Aggregates:
db.serverStatus()Die Output ist massiv – hunderte Felder. Die relevanten für Index-Performance:
{
metrics: {
queryExecutor: {
scanned: 15000000, // Total docs scanned
scannedObjects: 12000000, // Docs scanned (nicht via Index)
collectionScans: {
total: 523, // Total COLLSCAN operations
nonTailable: 523
}
}
},
indexCounters: {
accesses: 5000000, // Total index accesses
hits: 5000000,
misses: 0
}
}Key-Ratio: scannedObjects vs. scanned:
Idealerweise ist scannedObjects deutlich kleiner als
scanned – die Diskrepanz ist Index-Usage.
scanned: 15M
scannedObjects: 1M
// Bedeutet: 14M Scans via Index, 1M ohne Index
// Ratio: ~93% Index-Usage – gutWenn scannedObjects ≈ scanned, werden kaum Indexes
genutzt – systemisches Problem.
collectionScans.total:
Anzahl der COLLSCAN-Operations seit Server-Start. Ein stetig wachsender Counter bedeutet: Regelmäßig laufen Queries ohne Index.
Monitoring-Pattern:
// Periodisch (jede Minute):
const current = db.serverStatus().metrics.queryExecutor
const previous = loadPreviousMetrics()
const collscansPerMinute = current.collectionScans.total - previous.collectionScans.total
if (collscansPerMinute > 100) {
alert("High COLLSCAN rate detected")
}
savePreviousMetrics(current)Dies detektiert plötzliche Anstiege in COLLSCAN-Rate – oft Indikator für Code-Deployment mit ineffizienten Queries.
Für Capacity-Planning und Index-Overhead-Analysis:
db.orders.stats()Relevante Felder:
{
size: 5000000000, // Total data size (5 GB)
count: 10000000, // Document count
avgObjSize: 500, // Average doc size (bytes)
storageSize: 6000000000, // Storage size including padding
totalIndexSize: 1500000000, // Total index size (1.5 GB)
indexSizes: {
_id_: 300000000, // _id index: 300 MB
status_1: 200000000, // status index: 200 MB
customerId_1_orderDate_-1: 800000000, // compound: 800 MB
legacyField_1: 200000000 // legacy: 200 MB
}
}Index-Overhead-Ratio:
indexOverhead = totalIndexSize / size
= 1.5 GB / 5 GB
= 30%Ein 30%-Index-Overhead ist reasonable. Bei > 50% sollte man prüfen, ob manche Indexes dropp-bar sind.
Per-Index-ROI-Calculation:
Combine stats() mit $indexStats:
// Index-Size
const indexSizes = db.orders.stats().indexSizes
// Index-Usage
const indexUsage = db.orders.aggregate([{ $indexStats: {} }]).toArray()
// Calculate ROI: Operations per MB
indexUsage.forEach(idx => {
const sizeMB = indexSizes[idx.name] / (1024 * 1024)
const opsPerMB = idx.accesses.ops / sizeMB
print(`${idx.name}: ${opsPerMB.toFixed(2)} ops/MB`)
})
// Output:
// status_1: 627.16 ops/MB - high value
// customerId_1_orderDate_-1: 111.65 ops/MB - moderate value
// legacyField_1: 0.015 ops/MB - very low value, drop candidateIndexes mit niedrigem ops/MB-Ratio sind Prime-Candidates fürs Droppen – hoher Storage-Cost, niedriger Usage.
Ein Production-Ready-Index-Optimization-Workflow kombiniert alle Tools:
Phase 1: Detection (Weekly)
// 1. Identify Slow-Queries
const slowQueries = db.system.profile.find({
ts: { $gte: lastWeek },
millis: { $gte: 1000 },
planSummary: /COLLSCAN/
}).toArray()
// 2. Group by Collection und Fields
const queryPatterns = groupByFields(slowQueries)
// Output:
// orders: [{ status, customerId }, { status, amount }]
// products: [{ category, price }]Phase 2: Analysis (Ad-hoc)
// Für jedes Pattern: Prüfe mit explain
queryPatterns.forEach(pattern => {
const explainOutput = db[pattern.collection]
.find(pattern.filter)
.explain("executionStats")
if (explainOutput.executionStats.executionStages.stage === "COLLSCAN") {
print(`Missing Index: ${pattern.collection} on ${JSON.stringify(pattern.filter)}`)
}
})Phase 3: Implementation (Scheduled Maintenance)
// Create Missing Indexes
db.orders.createIndex({ status: 1, customerId: 1 }, { background: true })
// Validate
db.orders.find({ status: "pending", customerId: "CUST-123" })
.explain("executionStats")
// Check: IXSCAN, totalDocsExamined ≈ nReturnedPhase 4: Cleanup (Quarterly)
// Find Unused Indexes
const unusedIndexes = db.orders.aggregate([
{ $indexStats: {} },
{ $match: { "accesses.ops": { $lt: 1000 } } }
]).toArray()
// Manual-Review, dann Drop
unusedIndexes.forEach(idx => {
if (idx.name !== "_id_") { // Never drop _id!
print(`Consider dropping: ${idx.name} (${idx.accesses.ops} ops)`)
// After verification:
// db.orders.dropIndex(idx.name)
}
})Phase 5: Monitoring (Continuous)
// Automated-Dashboards
setInterval(() => {
const metrics = {
timestamp: new Date(),
avgQueryTime: getAvgQueryTime(),
collscanRate: getCollscanRate(),
indexHitRate: getIndexHitRate(),
slowQueryCount: getSlowQueryCount()
}
db.monitoring.insertOne(metrics)
if (metrics.collscanRate > threshold) {
alertTeam("High COLLSCAN rate")
}
}, 60000) // Every minuteDie folgende Tabelle fasst Metrics-Tools zusammen:
| Tool | Scope | Frequency | Primary-Use |
|---|---|---|---|
explain() |
Single-Query | Ad-hoc | Query-Optimization |
$indexStats |
Per-Index | Weekly/Monthly | Index-Usage-Analysis |
| Profiler | All-Queries | Continuous | Slow-Query-Detection |
serverStatus() |
System-Wide | Continuous (1min) | Aggregate-Metrics |
stats() |
Collection | Monthly | Capacity-Planning |
Index-Metrics sind nicht nur für reactive Debugging – “Diese Query ist langsam, warum?” – sondern für proactive Optimization. Systematic Monitoring detektiert Probleme früh, bevor sie Production-Impact haben. Regelmäßige Index-Reviews halten das Index-Set clean – nur nötige Indexes, keine cruft. Capacity-Metrics informieren Scale-Decisions – wann braucht man mehr RAM für Indexes? Alerting auf anomale Patterns (plötzliche COLLSCAN-Spikes) ermöglicht schnelle Response. Mit datengetriebenem Index-Management transformiert sich Performance von reactive firefighting zu proactive optimization – kontinuierlich messbar, systematisch verbesserbar.