42 Index-Metriken: Datengetriebene Performance-Optimierung

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.

42.1 explain(): Die Query-Mikroskop

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: 50000

Dies 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.

42.2 $indexStats: Index-Usage-Tracking

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?

42.3 Profiler: Slow-Query-Detection

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:

Action:

// 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: 5

Profiler-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)
}

42.4 serverStatus(): System-Wide-Metrics

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 – gut

Wenn 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.

42.5 stats(): Collection und Index-Size-Metrics

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 candidate

Indexes mit niedrigem ops/MB-Ratio sind Prime-Candidates fürs Droppen – hoher Storage-Cost, niedriger Usage.

42.6 Practical Workflow: Systematic Index-Optimization

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 ≈ nReturned

Phase 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 minute

Die 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.