40 Keys in MongoDB: Identity, Performance und Distribution

In relationalen Datenbanken sind Keys ein fundamentales Konzept – Primary Keys für Uniqueness, Foreign Keys für Relationships, Composite Keys für Multi-Column-Uniqueness. MongoDB als Document-Database hat ein anderes Paradigma, aber Keys sind nicht weniger wichtig. Sie dienen drei kritischen Zwecken: Identity (jedes Dokument braucht einen Unique-Identifier), Performance (Indexes beschleunigen Queries), und Distribution (Shard-Keys kontrollieren Data-Placement in Clustern).

Aber MongoDB’s Key-Konzepte unterscheiden sich fundamental von SQL. Es gibt keine deklarierten Foreign-Keys mit Referential-Integrity-Constraints. Primary-Keys sind nicht optional wie in SQL, sondern mandatory – jedes Dokument muss ein _id-Feld haben. Shard-Keys sind immutable nach Sharding-Aktivierung. Diese Unterschiede sind nicht nur syntaktisch, sondern fundamental für MongoDB’s Design-Philosophy – Flexibility über Rigidity, Developer-Responsibility über Database-Enforcement.

Dieses Kapitel durchläuft MongoDB’s Key-Typen systematisch, mit Fokus auf praktische Implikationen – wann nutzt man welchen Key-Typ, welche Trade-offs existieren, und wie designed man Keys für Performance und Scalability.

40.1 _id: Der unverzichtbare Primary Key

Jedes MongoDB-Dokument hat ein _id-Feld. Dies ist nicht optional – wenn man ein Dokument inserted ohne _id, generiert MongoDB automatisch eine ObjectId. Das _id-Feld ist der Primary Key – unique innerhalb der Collection, automatically indexed, immutable nach Insert.

// Insert ohne _id
db.users.insertOne({ username: "alice", email: "alice@example.com" })

// Resultat
{
  _id: ObjectId("65abc123def456789012345"),  // Auto-generated
  username: "alice",
  email: "alice@example.com"
}

Die ObjectId-Struktur:

ObjectId ist kein Random-String, sondern eine 12-Byte-Struktur mit embedded Information:

ObjectId = 4 bytes timestamp + 5 bytes random value + 3 bytes counter

Beispiel: 65abc123def456789012345
         |------| |--------| |---|
         Timestamp  Random   Counter

Diese Struktur garantiert praktisch universelle Uniqueness ohne Coordination zwischen Nodes. Zwei MongoDB-Instanzen können concurrently ObjectIds generieren ohne Collision-Risk – die Random-Value und Counter-Components machen Collisions astronomisch unwahrscheinlich.

ObjectId-Vorteile:

  1. Sortable by Creation Time: Queries wie “newest 100 users” können _id sortieren statt ein separates createdAt-Feld.
  2. No Coordination Required: Distributed-Systems können IDs lokal generieren ohne Central-Authority.
  3. Embedded Timestamp: Man kann Creation-Time aus der ID extrahieren ohne extra Field.
// Extract timestamp from ObjectId
const objectId = ObjectId("65abc123def456789012345")
const timestamp = objectId.getTimestamp()
print(timestamp)  // ISODate("2024-01-20T...")

ObjectId-Nachteile:

  1. 12 Bytes vs. 4-Byte-Integer: Größer als Sequential-IDs, mehr Storage und Index-Overhead.
  2. Non-Sequential: Nicht optimal für B-Tree-Indexes wenn extreme Write-Throughput nötig (Index-Splits).
  3. Not Human-Readable: “65abc123def456789012345” ist schwerer zu kommunizieren als “12345”.

40.2 Custom _id: Wenn ObjectId nicht passt

Man kann eigene _id-Werte verwenden – jeder BSON-Type ist valide:

// String ID
db.products.insertOne({ 
  _id: "PROD-12345",
  name: "Laptop"
})

// Integer ID
db.users.insertOne({
  _id: 100001,
  username: "alice"
})

// Compound ID
db.sales.insertOne({
  _id: { region: "EU", year: 2024, quarter: 1 },
  revenue: 1500000
})

Use-Cases für Custom IDs:

1. Natural Keys:

Wenn ein Field bereits unique ist, kann man es als _id nutzen:

// Email als _id
db.users.insertOne({
  _id: "alice@example.com",
  username: "alice"
})

Vorteil: Ein Index weniger (Email ist bereits via _id indexed). Nachteil: Immutability – wenn Email ändert, muss das gesamte Dokument re-created werden (MongoDB erlaubt kein _id-Update).

2. External-System-IDs:

Für Integration mit externen Systemen, die eigene IDs haben:

// Stripe Customer ID
db.customers.insertOne({
  _id: "cus_ABC123XYZ",  // Von Stripe
  name: "Alice Smith"
})

Dies vermeidet Join-Lookups – die Stripe-ID ist direkt die MongoDB-ID.

3. Compound-IDs für Uniqueness-Constraints:

Für Dokumente, die durch Multiple-Fields unique sind:

// User-Product-Rating - unique per User+Product
db.ratings.insertOne({
  _id: { userId: "USER-123", productId: "PROD-456" },
  rating: 5,
  comment: "Great product!"
})

Ein User kann ein Product nur einmal raten – die Compound-ID enforced dies.

Custom-ID-Pitfalls:

40.3 Indexes als Performance-Keys

Indexes sind Secondary-Keys – sie beschleunigen Queries, aber sind nicht für Uniqueness (außer Unique-Indexes). MongoDB’s Index-System ist ähnlich zu SQL-Datenbanken – B-Tree-Structures für schnelle Lookups.

// Simple Index
db.users.createIndex({ email: 1 })

// Compound Index
db.orders.createIndex({ customerId: 1, orderDate: -1 })

// Unique Index
db.users.createIndex({ username: 1 }, { unique: true })

Index-Keys bestimmen Query-Performance:

// Ohne Index
db.users.find({ email: "alice@example.com" })
// COLLSCAN - scannt alle Dokumente

// Mit Index
db.users.createIndex({ email: 1 })
db.users.find({ email: "alice@example.com" })
// IXSCAN - nutzt Index, 100-1000x schneller

Compound-Indexes und Key-Order:

Die Reihenfolge der Felder in einem Compound-Index ist kritisch:

// Index: { status: 1, createdAt: -1 }
db.orders.createIndex({ status: 1, createdAt: -1 })

// Nutzt Index effizient
db.orders.find({ status: "pending" }).sort({ createdAt: -1 })

// Nutzt Index NICHT effizient
db.orders.find({ createdAt: { $gte: lastWeek } }).sort({ status: 1 })

Die Index-Rule: Equality-Felder zuerst, dann Range-Felder, dann Sort-Felder.

// Optimal für: status = X, amount > Y, sort by createdAt
db.orders.createIndex({ status: 1, amount: 1, createdAt: -1 })

Index-Trade-offs:

Indexes sind nicht kostenlos:

  1. Storage-Overhead: Jeder Index speichert Keys und Pointers – signifikanter Storage bei großen Collections.
  2. Write-Overhead: Jedes Insert/Update muss alle Indexes updaten – mehr Indexes = langsamere Writes.
  3. RAM-Requirements: Working-Set-Indexes sollten in RAM passen für Performance.

Die Best Practice: Nur Indexes für häufige Queries. Zu viele Indexes schaden mehr als sie helfen.

Unique Indexes als Constraints:

Unique-Indexes enforcem Uniqueness ähnlich zu SQL-Constraints:

db.users.createIndex({ email: 1 }, { unique: true })

// Duplicate insert fails
db.users.insertOne({ email: "alice@example.com" })  // Success
db.users.insertOne({ email: "alice@example.com" })  // Error: E11000 duplicate key

Dies ist MongoDB’s Mechanismus für Unique-Constraints jenseits von _id.

Partial Indexes für Efficiency:

Für Indexes, die nur auf Subset von Dokumenten relevant sind:

// Index nur auf active users
db.users.createIndex(
  { email: 1 },
  { 
    partialFilterExpression: { status: "active" },
    unique: true
  }
)

Dies spart Storage – inactive Users sind nicht im Index. Und erlaubt Uniqueness nur unter aktiven Users (zwei inactive Users können gleiche Email haben, aber kein active User kann Email eines anderen active Users haben).

40.4 Shard Keys: Distribution-Control in Clustern

In Sharded-Clusters bestimmt der Shard-Key, wie Daten über Shards distributed werden. Dies ist einer der wichtigsten Design-Entscheidungen für horizontale Scalability.

// Enable Sharding für Database
sh.enableSharding("ecommerce")

// Shard Collection auf customerId
sh.shardCollection("ecommerce.orders", { customerId: 1 })

Nach diesem Command werden Orders basierend auf customerId auf Shards verteilt. Alle Orders für einen Customer landen auf demselben Shard.

Shard-Key-Kriterien:

Ein guter Shard-Key hat drei Properties:

  1. High Cardinality: Viele unique Values. Ein Shard-Key mit nur 10 distinct Values kann maximal auf 10 Shards distributed werden.

  2. Good Distribution: Values sind gleichmäßig verteilt, nicht skewed. Wenn 90% der Dokumente denselben Shard-Key-Value haben, landen sie auf einem Shard (Hotspot).

  3. Query-Aligned: Häufige Queries inkludieren den Shard-Key, erlauben Targeted-Queries statt Scatter-Gather.

Anti-Pattern: Monotone Keys

Sequential-IDs als Shard-Key sind problematisch:

// Schlecht: Sequential ID
sh.shardCollection("logs", { _id: 1 })

Neue Logs haben immer höhere _id (da ObjectIds monoton steigen). Alle Writes gehen zu einem Shard (dem mit den höchsten Values) – ein Write-Hotspot. Andere Shards sind idle.

Lösung: Hash-Based Sharding

sh.shardCollection("logs", { _id: "hashed" })

MongoDB hasht die _id vor Distribution. Selbst sequential IDs werden gleichmäßig distributed. Der Trade-off: Range-Queries sind ineffizient (Scatter-Gather), weil benachbarte IDs auf verschiedenen Shards sind.

Compound-Shard-Keys für Granularität:

sh.shardCollection("orders", { customerId: 1, orderDate: 1 })

Dies distributed Orders nach Customer UND Date. Vorteil: Feinere Granularität (mehr Chunks), bessere Distribution für Power-Users mit vielen Orders. Nachteil: Komplexere Query-Patterns nötig für Targeted-Queries.

Immutability des Shard-Keys:

Bis MongoDB 4.2 war der Shard-Key komplett immutable. Ab 4.2 kann man Shard-Key-Values updaten, aber die Shard-Key-Field-Definition bleibt immutable. Man kann nicht nachträglich von { customerId: 1 } zu { region: 1 } wechseln ohne Re-Sharding.

Zoned-Sharding für Geographic-Distribution:

Für Compliance oder Latency-Requirements kann man Shards geografisch binden:

// EU-Shard für EU-Customers
sh.addShardToZone("shard-eu", "EU")
sh.updateZoneKeyRange(
  "ecommerce.users",
  { country: "DE" },
  { country: "FR" },
  "EU"
)

Alle Users mit country zwischen DE und FR (alphabetisch) landen auf shard-eu.

40.5 Foreign Keys und Relationships: MongoDB’s Approach

MongoDB hat keine declarative Foreign-Keys wie SQL. Relationships sind Manual-References oder Embedded-Documents.

Manual-References (Normalisierung):

// Customers Collection
{ _id: ObjectId("..."), name: "Alice" }

// Orders Collection
{ 
  _id: ObjectId("..."),
  customerId: ObjectId("..."),  // Reference zu Customer
  amount: 150
}

Die customerId ist ein “Foreign Key” – aber es gibt keine Referential-Integrity. MongoDB enforced nicht, dass der Customer existiert. Man kann einen Order mit non-existent customerId insertn.

Queries mit References:

// Find Order
const order = db.orders.findOne({ _id: orderId })

// Find Customer (separate Query)
const customer = db.customers.findOne({ _id: order.customerId })

Oder mit $lookup (Join):

db.orders.aggregate([
  { $lookup: {
      from: "customers",
      localField: "customerId",
      foreignField: "_id",
      as: "customer"
  }}
])

Embedded-Documents (Denormalisierung):

Statt References kann man Daten embedden:

{
  _id: ObjectId("..."),
  customer: {
    _id: ObjectId("..."),
    name: "Alice",
    email: "alice@example.com"
  },
  amount: 150
}

Kein Join nötig – alle Daten in einem Dokument. Der Trade-off: Redundanz – wenn Alice’s Email ändert, müssen alle ihre Orders updated werden.

Wann Normalize, wann Denormalize:

Criteria Normalize (References) Denormalize (Embed)
Data ändert sich häufig
1:N mit N sehr groß (>100)
Queries brauchen nur Parent
Queries brauchen Parent+Child
Data wird separat queried

Beispiel: Orders und Customers – normalize. Orders und Line-Items – denormalize (Items sind nur im Context des Orders relevant).

40.6 Compound Keys für Multi-Field-Uniqueness

Manchmal ist Uniqueness über Multiple-Fields definiert. In SQL würde man einen Composite-Primary-Key nutzen. In MongoDB nutzt man entweder Compound-_id oder Unique-Compound-Index.

**Option 1: Compound _id**

db.enrollments.insertOne({
  _id: { studentId: 12345, courseId: "CS101" },
  enrolledDate: new Date(),
  grade: null
})

Ein Student kann einen Course nur einmal enrolln – die Compound-ID enforced dies.

Option 2: Unique Compound Index

db.enrollments.createIndex(
  { studentId: 1, courseId: 1 },
  { unique: true }
)

db.enrollments.insertOne({
  studentId: 12345,
  courseId: "CS101",
  enrolledDate: new Date()
})

Hier ist _id eine normale ObjectId, aber der Unique-Index enforced die Constraint.

Wann welchen Approach:

40.7 Key-Design-Patterns: Best Practices

Pattern 1: UUID für Distributed-Generation

In Microservices-Architekturen, wo Multiple-Services concurrently Documents insertn:

import { v4 as uuidv4 } from 'uuid'

db.events.insertOne({
  _id: uuidv4(),  // UUID v4
  type: "user_login",
  timestamp: new Date()
})

UUIDs sind universell unique ohne Coordination. Der Trade-off: 16 bytes (vs. 12 für ObjectId), non-sequential (schlechter für Index-Performance).

Pattern 2: Hierarchical Keys für Ordering

Für Dokumente mit natürlicher Hierarchie:

// Year-Month-Day-Sequence
_id: "2024-01-20-00001"

// Region-Store-Transaction
_id: "EU-STORE-Berlin-TXN-12345"

Diese Keys sind human-readable und sortable. Aber: String-IDs sind größer als ObjectIds.

Pattern 3: Hash-Prefixed-Keys für Distribution

Für Sequential-IDs, die aber distributed werden sollen:

// Original: 12345
// Hash-Prefixed: a3-12345 (ersten 2 Chars vom Hash)

function generateDistributedId(sequentialId) {
  const hash = md5(sequentialId.toString()).substring(0, 2)
  return `${hash}-${sequentialId}`
}

db.logs.insertOne({
  _id: generateDistributedId(12345),
  message: "..."
})

Dies distributed Sequential-IDs über Shards (via Hash-Prefix), behält aber Sequentiality innerhalb jedes Prefixes.

Die folgende Tabelle fasst Key-Typen zusammen:

Key-Type Zweck Mandatory Mutable Performance-Impact
_id Identity, Primary Key Ja Nein Auto-Indexed
Simple Index Query-Performance Nein Ja (kann dropped/rebuilt werden) Storage + Write-Overhead
Compound Index Multi-Field-Queries Nein Ja Höherer Overhead
Unique Index Uniqueness-Constraint Nein Ja Wie Index + Constraint-Check
Shard Key Data-Distribution Ja (in Sharded-Cluster) Nein (Pre-4.2)
Values: Ja (4.2+)
Kritisch für Cluster-Performance
Foreign Key Relationships Nein (Manual) Ja Application-Enforced

Keys in MongoDB sind weniger restrictive als in SQL, aber nicht weniger wichtig. Der _id-Primary-Key ist mandatory und fundamental. Indexes sind optional aber essentiell für Performance. Shard-Keys sind optional (nur für Sharding) aber transformativ für Scalability. Foreign-Keys sind konzeptionell vorhanden, aber Application-Enforced statt Database-Enforced. Das Design von Keys – welche Values für _id, welche Felder zu indexieren, welcher Shard-Key für Distribution – ist eine der wichtigsten Architektur-Entscheidungen für MongoDB-Systeme. Schlechte Key-Choices führen zu Performance-Problemen, Hotspots in Sharded-Clusters, oder Uniqueness-Violations. Gute Key-Choices ermöglichen schnelle Queries, balanced Distribution und klare Data-Identity. Mit sorgfältigem Key-Design wird MongoDB von flexibel zu performant und skalierbar.