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.
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:
_id sortieren statt ein separates
createdAt-Feld.// Extract timestamp from ObjectId
const objectId = ObjectId("65abc123def456789012345")
const timestamp = objectId.getTimestamp()
print(timestamp) // ISODate("2024-01-20T...")ObjectId-Nachteile:
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:
_id kann nicht updated
werden. Wenn der Natural-Key ändert (etwa Email), muss man das Dokument
re-createn.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 schnellerCompound-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:
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 keyDies 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).
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:
High Cardinality: Viele unique Values. Ein Shard-Key mit nur 10 distinct Values kann maximal auf 10 Shards distributed werden.
Good Distribution: Values sind gleichmäßig verteilt, nicht skewed. Wenn 90% der Dokumente denselben Shard-Key-Value haben, landen sie auf einem Shard (Hotspot).
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.
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).
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:
_id sinnvoll ist, aber man zusätzlich
Uniqueness-Constraints braucht. Beispiel: Order-Items (jedes Item hat
eigene ID, aber productId+orderId sollte unique sein).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.