23 Shard-Key-Auswahl: Theorie trifft Praxis

Die Wahl des Shard Keys ist die kritischste Entscheidung beim Aufbau eines Sharded Clusters. Sie bestimmt, wie Daten verteilt werden, welche Queries effizient sind, und wie gut das System skaliert. Ein gut gewählter Shard Key ermöglicht lineare Skalierung über Dutzende oder Hunderte Shards. Ein schlecht gewählter Key führt zu Hotspots, ungleicher Lastverteilung und Performance-Problemen, die schwer zu beheben sind.

Die Theorie ist klar: Der Shard Key soll hohe Cardinality haben, Daten gleichmäßig verteilen und häufige Queries isolieren. Aber in der Praxis gibt es selten einen perfekten Key, der alle Kriterien erfüllt. Oft muss man Trade-offs machen – zwischen Write-Performance und Read-Isolation, zwischen Einfachheit und Flexibilität. Dieses Kapitel durchläuft konkrete Szenarien und zeigt, wie man Shard Keys für reale Anwendungen wählt.

23.1 Szenario: E-Commerce Orders Collection

Betrachten wir eine typische E-Commerce-Plattform mit einer orders-Collection. Jedes Dokument repräsentiert eine Kundenbestellung:

{
  orderId: "ORD-2024-001234",
  customerId: "CUST-567890",
  orderDate: ISODate("2024-12-01T10:00:00Z"),
  items: [
    { productId: "PROD-111", quantity: 2, price: 49.99 },
    { productId: "PROD-222", quantity: 1, price: 149.99 }
  ],
  totalAmount: 249.97,
  status: "shipped",
  shippingAddress: {
    country: "DE",
    city: "Berlin",
    postalCode: "10115"
  }
}

Die Collection wächst kontinuierlich – tausende oder zehntausende Orders pro Tag. Nach einem Jahr könnten es Millionen Dokumente sein, nach fünf Jahren Hunderte Millionen. Ein Replica Set kann dies nicht mehr effizient bedienen. Wir müssen sharden.

Die Frage: Welches Feld als Shard Key?

23.2 Option 1: OrderId als Shard Key

Der orderId ist einzigartig, hochkardinal und scheint eine offensichtliche Wahl. Jede Order hat eine eindeutige ID. Die Verteilung wäre perfekt, wenn IDs zufällig generiert würden.

sh.shardCollection("shopDB.orders", { orderId: 1 })

Das Problem: OrderIds sind oft sequentiell oder timestamp-basiert. Neue Orders bekommen fortlaufende IDs: ORD-2024-001234, ORD-2024-001235, ORD-2024-001236. Alle neuen Writes landen im selben Chunk – dem mit den höchsten OrderId-Werten. Dies erzeugt einen Hot Shard: Ein Shard bekommt alle Writes, die anderen sind idle. Die Datenbank skaliert nicht, obwohl sie gesharded ist.

Hash-Based Sharding löst dieses Problem:

sh.shardCollection("shopDB.orders", { orderId: "hashed" })

Statt die OrderId direkt zu verwenden, hasht MongoDB sie. Der Hash-Wert ist gleichmäßig verteilt, selbst wenn die ursprünglichen IDs sequentiell sind. Neue Orders werden zufällig auf Shards verteilt. Write-Hotspots werden eliminiert.

Der Trade-off: Range-Queries funktionieren nicht mehr effizient. Eine Query wie “Finde alle Orders von ORD-2024-001000 bis ORD-2024-002000” muss alle Shards scannen, weil der Hash die Sortierung zerstört. Für die meisten E-Commerce-Anwendungen, wo Queries typischerweise per Customer oder Date erfolgen, nicht per OrderId-Range, ist dies akzeptabel.

23.3 Option 2: CustomerId als Shard Key

Die meisten Queries in E-Commerce-Systemen sind customer-zentrisch: “Finde alle Orders von Customer X”, “Zeige Order-History für User Y”. Ein Shard Key auf customerId macht solche Queries shard-isoliert:

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

Queries mit customerId gehen nur an einen Shard. Dies ist extrem effizient – fast so schnell wie auf einem ungesharded System. Die App-Performance bleibt hervorragend, selbst bei Dutzenden Shards.

Das Problem: Ungleiche Verteilung. Kunden bestellen unterschiedlich viel. Ein Power-User könnte tausende Orders haben, ein Gelegenheitskäufer fünf. Wenn CustomerIds sequentiell vergeben werden und neue Kunden kontinuierlich dazukommen, bekommen neue Chunks mit hohen CustomerIds mehr Writes – ein moderater Hotspot.

Wieder löst Hashing dies:

sh.shardCollection("shopDB.orders", { customerId: "hashed" })

Der Hash verteilt Kunden gleichmäßig, unabhängig von deren ID-Struktur. Aber: Range-Queries über CustomerIds sind nun ineffizient. Queries wie “Finde Orders aller Kunden von CUST-100000 bis CUST-200000” müssen alle Shards scannen. Für typische App-Queries (einzelner Customer) ist dies irrelevant.

Der subtile Nachteil von hashed customerId: Updates einzelner Orders erfordern den Shard-Key-Wert. Wenn die App oft “Update Order ORD-X set status = ‘delivered’” macht, ohne customerId zu kennen, muss die Query alle Shards scannen. Dies ist langsamer als wenn OrderId der Shard Key wäre. Man muss abwägen: Ist Customer-Query-Isolation wichtiger oder Order-Update-Effizienz?

23.4 Option 3: Compound Key – CustomerId + OrderDate

Oft ist die beste Lösung ein Compound Key – mehrere Felder kombiniert. Ein { customerId: 1, orderDate: 1 }-Key gibt feinere Granularität:

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

Dieser Key partitioniert zunächst nach Customer, dann innerhalb jedes Customers nach Datum. Queries mit beiden Feldern sind perfekt isoliert:

db.orders.find({ 
  customerId: "CUST-567890",
  orderDate: { $gte: ISODate("2024-01-01"), $lt: ISODate("2024-12-31") }
})

Diese Query geht nur an einen Shard und scannt dort nur einen kleinen Bereich. Optimal.

Der Vorteil von Compound Keys: Sie handhaben Power-User besser. Ein Kunde mit 10.000 Orders über zehn Jahre verteilt sich über mehrere Chunks (nach Jahr aufgeteilt). Kein einzelner Chunk wird riesig. Dies verhindert Probleme beim Balancing – riesige Chunks sind schwer zu bewegen.

Der Nachteil: Queries, die nur customerId nutzen, profitieren noch von Isolation, aber Queries nur nach orderDate müssen alle Shards scannen. Ein Compound Key optimiert für Queries, die das Leading Field (hier customerId) enthalten. Queries ohne Leading Field sind Scatter-Gather.

Die Entscheidung hängt vom Query-Pattern ab. Wenn 90% der Queries customerId enthalten, ist der Compound Key gut. Wenn viele Queries nur nach orderDate filtern (“alle Orders der letzten Woche”), ist der Key suboptimal.

23.5 Hashed vs. Range Sharding: Der fundamentale Trade-off

Hash-Based Sharding garantiert gleichmäßige Verteilung, zerstört aber Sortierung. Range-Based Sharding erhält Sortierung, riskiert aber Hotspots. Dies ist der zentrale Trade-off beim Shard-Key-Design.

Hash Sharding: - Verteilung: Exzellent, praktisch perfekt gleichmäßig - Range Queries: Ineffizient, müssen alle Shards scannen - Monotone Writes: Kein Problem, gut verteilt - Use-Case: Write-heavy Workloads, sequentielle Keys, Point-Queries dominant

Range Sharding: - Verteilung: Potenziell ungleich, erfordert sorgfältigen Key-Design - Range Queries: Effizient, können shard-isoliert sein - Monotone Writes: Hotspot-Risiko bei schlecht gewähltem Key - Use-Case: Range-Query-heavy Workloads, natürliche Daten-Ranges

Für unser Orders-Beispiel mit sequentiellen OrderIds oder CustomerIds ist Hash meist die sichere Wahl. Für andere Domains – etwa geografische Daten, wo Range-Queries über Koordinaten häufig sind – könnte Range besser sein.

23.6 Szenario 2: Inventory Collection mit geografischer Verteilung

Betrachten wir ein zweites Szenario: Eine globale Retail-Kette mit Inventory-Daten:

{
  itemId: "ITEM-A12345",
  warehouseId: "WH-DE-001",
  warehouseLocation: {
    country: "DE",
    region: "NRW",
    coordinates: [51.5074, 7.4653]
  },
  quantity: 150,
  lastUpdated: ISODate("2024-12-05T15:30:00Z"),
  reorderLevel: 50
}

Die App führt hauptsächlich Queries aus wie “Finde Inventory in Warehouse X” oder “Finde alle Low-Stock-Items in Region Y”. Die warehouseId scheint ein natürlicher Shard Key:

sh.shardCollection("retailDB.inventory", { warehouseId: "hashed" })

Hashed warehouseId verteilt Warehouses gleichmäßig auf Shards. Queries nach spezifischem Warehouse sind shard-isoliert. Dies funktioniert gut.

Aber: Wenn die App oft regionale Queries macht (“alle Warehouses in Deutschland”), ist dieser Key suboptimal. Warehouses in DE sind über alle Shards verstreut, die Query muss alle scannen. Ein geographic-aware Key wäre besser:

sh.shardCollection("retailDB.inventory", { "warehouseLocation.country": 1, warehouseId: 1 })

Dieser Compound Key partitioniert zunächst nach Land, dann nach Warehouse. Alle deutschen Warehouses könnten auf denselben Shard(s) landen, was regionale Queries isoliert. Globale Queries (“alle Warehouses weltweit mit Low Stock”) scannen weiterhin alle Shards, aber das ist unvermeidbar bei globalen Fragen.

Der Trade-off: Ungleiche geografische Verteilung könnte zu ungleicher Shard-Nutzung führen. Wenn 60% der Warehouses in USA sind, 30% in Europa und 10% in Asien, bekommen US-Shards mehr Last. Dies erfordert mehr Shards für US-Daten oder Akzeptanz der Ungleichheit.

23.7 Zonned Sharding: Geografische Daten-Platzierung

Für geografisch verteilte Anwendungen bietet MongoDB Zoned Sharding (früher Tag-Aware Sharding). Man kann Shard-Ranges zu geografischen Zonen zuweisen:

// Deutschland-Zone definieren
sh.addShardToZone("shard1RS", "EU-DE")
sh.addShardToZone("shard2RS", "EU-DE")

// USA-Zone definieren
sh.addShardToZone("shard3RS", "US")
sh.addShardToZone("shard4RS", "US")

// Ranges zu Zones zuweisen
sh.updateZoneKeyRange(
  "retailDB.inventory",
  { "warehouseLocation.country": "DE" },
  { "warehouseLocation.country": "DE" },
  "EU-DE"
)

sh.updateZoneKeyRange(
  "retailDB.inventory",
  { "warehouseLocation.country": "US" },
  { "warehouseLocation.country": "US" },
  "US"
)

MongoDB stellt sicher, dass deutsche Warehouse-Daten nur auf Shards in der EU-DE-Zone liegen, US-Daten nur auf US-Shards. Dies erlaubt Data Residency Compliance – gesetzliche Anforderungen, dass Daten in bestimmten Jurisdiktionen bleiben müssen.

Zoned Sharding ist mächtig, aber komplex. Es erfordert sorgfältige Planung und saubere Daten-Grenzen. Für viele Anwendungen ist simples Hashing einfacher und ausreichend.

23.8 Anti-Patterns: Was man vermeiden sollte

Manche Shard-Key-Wahl sind so schlecht, dass sie als Anti-Patterns gelten:

Monotone Keys ohne Hashing: Timestamp, Auto-Increment-IDs, ObjectId (dessen Leading Bytes ein Timestamp sind) als Range-Shard-Key führen immer zu Hotspots. Jeder neue Write geht an denselben Chunk. Dies skaliert nicht.

Low-Cardinality Keys: Ein Feld mit wenigen Werten – etwa status mit vier Werten – limitiert auf maximal vier Chunks. Man kann nicht auf mehr als vier Shards verteilen. Das System kann nicht skalieren.

Keys mit sehr ungleicher Verteilung: Ein region-Feld, wo 90% der Dokumente “North America” sind, führt zu extremer Ungleichheit. Ein Shard bekommt 90% der Daten, die anderen teilen sich 10%. Die teure Sharding-Infrastruktur wird verschwendet.

Mutable Shard Keys (vor 4.2): Vor MongoDB 4.2 waren Shard-Key-Werte immutable – man konnte sie nicht updaten. Ein Shard Key auf einem Feld, das sich oft ändert, machte Updates unmöglich oder sehr teuer (Delete + Reinsert). Seit 4.2 sind Shard-Key-Updates möglich, aber immer noch teurer als normale Updates.

23.9 Praktische Entscheidungs-Matrix

Die folgende Tabelle fasst Entscheidungskriterien zusammen:

Situation Empfohlener Shard Key Begründung
Sequentielle IDs, Write-heavy { id: "hashed" } Eliminiert Write-Hotspots
Customer-zentrische Queries { customerId: "hashed" } Query-Isolation für typische Pattern
Time-Series mit Customer { customerId: 1, timestamp: 1 } Verhindert riesige Chunks pro Customer
Geografische Queries häufig { country: 1, region: 1, id: 1 } Lokalisiert geo-Queries
Multi-Tenant mit TenantId { tenantId: 1, ... } Perfekte Tenant-Isolation
High-Cardinality, gut verteilt { naturalKey: 1 } Range Sharding ohne Hashing
Keine klare Wahl { _id: "hashed" } Default, funktioniert immer okay

Die Wahl des Shard Keys ist keine exakte Wissenschaft, sondern Kunst basierend auf Workload-Verständnis. Monitoring nach dem Sharding ist kritisch – db.collection.getShardDistribution() zeigt, ob die Verteilung wie erwartet ist. Extreme Ungleichheit oder Hotspots müssen adressiert werden, möglicherweise durch Resharding.

Die beste Strategie: Mit Monitoring und Analytics starten. Verstehen, welche Queries häufig sind, welche Felder gefiltert werden, wie Daten wachsen. Mit diesem Wissen eine informierte Shard-Key-Entscheidung treffen. Und bereit sein, zu iterieren – Resharding in MongoDB 5.0+ macht Korrekturen möglich, wenn auch nicht trivial.