LSI vs. GSI in DynamoDB: Den richtigen Sekundärindex wählen

Man hat eine DynamoDB-Tabelle aufgebaut, die Abfragen über den Primärschlüssel laufen schnell — und dann kommt die Anforderung, dieselben Daten nach einem anderen Attribut zu filtern. Genau hier beginnt die Entscheidung zwischen einem Local Secondary Index (LSI) und einem Global Secondary Index (GSI), und wer sie falsch trifft, zahlt entweder mit Flexibilitätsverlust oder mit unerwartetem Durchsatz-Overhead.

TL;DR: LSI vs. GSI auf einen Blick

MerkmalLSIGSI
ErstellungszeitpunktNur bei TabellenerstellungJederzeit
Partition KeyIdentisch mit BasistabelleBeliebiges Attribut
Sort KeyAnderes Attribut als BasistabelleBeliebiges Attribut (optional)
KonsistenzmodellStrongly consistent möglichNur eventually consistent
KapazitätTeilt RCU/WCU mit BasistabelleEigene RCU/WCU (Provisioned) oder On-Demand
Datenmenge pro Partition KeyMax. 10 GBUnbegrenzt
Max. Anzahl pro Tabelle520 (Standard-Limit)
Nachträgliches HinzufügenNicht möglichMöglich

Wie DynamoDB-Sekundärindizes funktionieren

DynamoDB speichert Daten partitioniert nach dem Partition Key der Basistabelle. Innerhalb einer Partition werden Items nach dem Sort Key geordnet. Sekundärindizes sind keine separaten Abfrage-Layer — sie sind projizierte Kopien der Daten, die DynamoDB intern synchron (LSI) oder asynchron (GSI) pflegt.

Ein LSI erweitert die Abfragemöglichkeiten innerhalb einer bestehenden Partition. Der Partition Key bleibt identisch, nur der Sort Key ändert sich. Das bedeutet: alle Items mit demselben Partition Key bleiben in derselben logischen Partition — der Index fügt lediglich eine alternative Sortierreihenfolge hinzu. Die 10-GB-Grenze pro Partition Key gilt deshalb für LSIs genauso wie für die Basistabelle selbst.

Ein GSI hingegen definiert einen vollständig neuen Partition Key. DynamoDB verteilt die projizierten Daten auf eine eigene interne Partitionsstruktur, unabhängig von der Basistabelle. Schreiboperationen auf die Basistabelle propagieren asynchron in den GSI — daher ist starke Konsistenz bei GSI-Abfragen nicht verfügbar.

graph LR BT["Basistabelle
PK: KundenId | SK: BestellId"] LSI["LSI: KundenId-Datum-LSI
PK: KundenId | SK: BestellDatum"] GSI["GSI: Status-Datum-GSI
PK: Status | SK: BestellDatum"] BT -->|"Gleicher Partition Key
Synchron repliziert"| LSI BT -->|"Neuer Partition Key
Asynchron repliziert"| GSI Q1["Query: Bestellungen
eines Kunden nach Datum"] --> LSI Q2["Query: Alle offenen
Bestellungen nach Datum"] --> GSI
  1. Basistabelle: Partitioniert nach KundenId, sortiert nach BestellId.
  2. LSI: Gleicher Partition Key (KundenId), alternativer Sort Key (BestellDatum). Abfragen bleiben innerhalb der Kundenpartition.
  3. GSI: Neuer Partition Key (Status). DynamoDB verteilt die Daten auf eigene GSI-Partitionen — eine Abfrage nach Status = 'OFFEN' greift auf alle Kunden zu, ohne die Basistabelle zu scannen.

LSI in DynamoDB: Wann er sinnvoll ist

Der LSI ist die richtige Wahl, wenn Abfragen immer innerhalb des Partition Keys der Basistabelle bleiben — und wenn starke Lesekonsistenz eine harte Anforderung ist. Ein typisches Beispiel: eine Bestelltabelle mit KundenId als Partition Key. Ohne Index kann man Bestellungen eines Kunden nur nach BestellId sortieren. Mit einem LSI auf BestellDatum lassen sich die letzten N Bestellungen eines Kunden effizient abrufen — ohne Scan, ohne Konsistenzkompromiss.

Ein LSI ist wie ein zweiter Karteikartenreiter im selben Ordner. Man blättert schneller, aber man verlässt den Ordner nie.

Der kritische Punkt: LSIs müssen bei der Tabellenerstellung definiert werden. Wer das vergisst, muss die Tabelle neu erstellen und die Daten migrieren — es gibt keinen update-table-Pfad für LSIs.

LSI erstellen (bei Tabellenerstellung)

🔽 CLI: Tabelle mit LSI erstellen
aws dynamodb create-table \
  --table-name Bestellungen \
  --attribute-definitions \
      AttributeName=KundenId,AttributeType=S \
      AttributeName=BestellId,AttributeType=S \
      AttributeName=BestellDatum,AttributeType=S \
  --key-schema \
      AttributeName=KundenId,KeyType=HASH \
      AttributeName=BestellId,KeyType=RANGE \
  --local-secondary-indexes \
      '[{
          "IndexName": "KundenId-Datum-LSI",
          "KeySchema": [
              {"AttributeName": "KundenId", "KeyType": "HASH"},
              {"AttributeName": "BestellDatum", "KeyType": "RANGE"}
          ],
          "Projection": {
              "ProjectionType": "INCLUDE",
              "NonKeyAttributes": ["Status", "Betrag"]
          }
      }]' \
  --billing-mode PAY_PER_REQUEST \
  --region us-east-1

Mit diesem Index lässt sich folgende Abfrage effizient ausführen — alle Bestellungen eines Kunden, sortiert nach Datum, mit starker Konsistenz:

aws dynamodb query \
  --table-name Bestellungen \
  --index-name KundenId-Datum-LSI \
  --key-condition-expression 'KundenId = :kid AND BestellDatum BETWEEN :von AND :bis' \
  --expression-attribute-values \
      '{":kid": {"S": "K-001"}, ":von": {"S": "2024-01-01"}, ":bis": {"S": "2024-06-30"}}' \
  --consistent-read \
  --region us-east-1

Das --consistent-read-Flag funktioniert beim LSI. Beim GSI würde derselbe Parameter einen Fehler zurückgeben.

GSI in DynamoDB: Flexibilität auf Kosten der Konsistenz

Der GSI löst das Problem, das der LSI strukturell nicht lösen kann: Abfragen, die den Partition Key der Basistabelle nicht kennen oder ignorieren müssen. Wenn ein Support-Team alle offenen Bestellungen sehen will — unabhängig vom Kunden — braucht man einen Index, der nach Status partitioniert ist. Das ist ein GSI.

GSIs haben eigene Lese- und Schreibkapazitäten. Bei Provisioned-Tabellen bedeutet das: jede Schreiboperation auf die Basistabelle, die ein projiziertes Attribut ändert, verbraucht zusätzliche WCUs aus dem GSI-Kapazitätspool. Wer das übersieht, wundert sich über unerklärliche ProvisionedThroughputExceededException-Fehler — nicht auf der Tabelle, sondern auf dem Index.

GSI-Throttling ist heimtückisch: Die Basistabelle hat freie Kapazität, aber der Index ist der Engpass. CloudWatch-Metriken müssen explizit auf Index-Ebene überwacht werden.

GSI nachträglich hinzufügen

aws dynamodb update-table \
  --table-name Bestellungen \
  --attribute-definitions \
      AttributeName=Status,AttributeType=S \
      AttributeName=BestellDatum,AttributeType=S \
  --global-secondary-index-updates \
      '[{
          "Create": {
              "IndexName": "Status-Datum-GSI",
              "KeySchema": [
                  {"AttributeName": "Status", "KeyType": "HASH"},
                  {"AttributeName": "BestellDatum", "KeyType": "RANGE"}
              ],
              "Projection": {
                  "ProjectionType": "INCLUDE",
                  "NonKeyAttributes": ["KundenId", "Betrag"]
              }
          }
      }]' \
  --region us-east-1

DynamoDB befüllt den GSI asynchron im Hintergrund. Der Index-Status wechselt von CREATING zu ACTIVE — erst dann sind Abfragen zuverlässig. Den aktuellen Status prüft man so:

aws dynamodb describe-table \
  --table-name Bestellungen \
  --query 'Table.GlobalSecondaryIndexes[*].{Name:IndexName,Status:IndexStatus}' \
  --region us-east-1

Abfragen über den GSI nach Status und Datum:

aws dynamodb query \
  --table-name Bestellungen \
  --index-name Status-Datum-GSI \
  --key-condition-expression 'Status = :s AND BestellDatum >= :datum' \
  --expression-attribute-values \
      '{":s": {"S": "OFFEN"}, ":datum": {"S": "2024-01-01"}}' \
  --region us-east-1

Projektionstypen: Was wirklich im Index landet

Beide Indextypen unterstützen drei Projektionstypen. Die Wahl beeinflusst direkt Speicherkosten und ob DynamoDB für fehlende Attribute einen zusätzlichen Fetch auf die Basistabelle ausführen muss.

graph TD P["Projektionstyp wählen"] K["KEYS_ONLY
Nur Index- und Tabellen-Keys"] I["INCLUDE
Keys + definierte Attribute"] A["ALL
Alle Attribute"] P --> K P --> I P --> A K -->|"Fehlende Attribute?"| F["Automatischer Fetch
auf Basistabelle = extra RCUs"] I -->|"Alle Abfrage-Attribute
projiziert"| OK1["Kein extra Fetch nötig"] A -->|"Vollständige Kopie
höchster Speicherbedarf"| OK2["Kein extra Fetch nötig"]
  1. KEYS_ONLY: Nur Primärschlüssel der Basistabelle und Index-Keys werden projiziert. Geringster Speicherbedarf. Wenn die Abfrage weitere Attribute benötigt, führt DynamoDB automatisch einen Fetch auf die Basistabelle aus — das kostet zusätzliche RCUs.
  2. INCLUDE: Zusätzlich zu den Keys werden explizit genannte Attribute projiziert. Guter Kompromiss, wenn die Abfrage-Attribute bekannt und stabil sind.
  3. ALL: Alle Attribute der Basistabelle werden kopiert. Keine zusätzlichen Fetches nötig, aber doppelter Speicherbedarf. Bei breiten Items und häufigen Schreiboperationen kann das teuer werden.

Typischer Diagnosefehler aus der Praxis

Ein Team beobachtet sporadische Latenzspitzen bei Schreiboperationen auf eine DynamoDB-Tabelle. Die CloudWatch-Metrik ConsumedWriteCapacityUnits der Tabelle liegt weit unter dem Limit. Der erste Verdacht: Hot Partition. Stundenlange Analyse der Partition-Key-Verteilung — kein Befund.

Die eigentliche Ursache: Die Tabelle hat drei GSIs mit ProjectionType: ALL. Jede Schreiboperation repliziert die Daten in alle drei Indizes. Die GSI-spezifischen Metriken — ConsumedWriteCapacityUnits auf Index-Ebene — waren nie in das Monitoring eingebunden worden. Einer der GSIs war auf einem zu niedrigen WCU-Limit provisioniert.

Die Lösung war trivial, sobald man am richtigen Ort schaute:

aws cloudwatch get-metric-statistics \
  --namespace AWS/DynamoDB \
  --metric-name ConsumedWriteCapacityUnits \
  --dimensions \
      Name=TableName,Value=Bestellungen \
      Name=GlobalSecondaryIndexName,Value=Status-Datum-GSI \
  --start-time 2024-06-01T00:00:00Z \
  --end-time 2024-06-01T06:00:00Z \
  --period 300 \
  --statistics Sum \
  --region us-east-1

GSI-Throttling taucht nicht in den Tabellen-Metriken auf. Wer nur die Tabelle überwacht, sieht das Problem nie.

Entscheidungsbaum: LSI oder GSI?

graph TD A["Neues Abfragemuster benötigt"] --> B{"Bleibt die Abfrage
innerhalb des Partition Keys?"} B -->|"Ja"| C{"Starke Konsistenz
erforderlich?"} B -->|"Nein"| G["GSI verwenden"] C -->|"Ja"| D{"Tabelle bereits
erstellt?"} C -->|"Nein"| G D -->|"Nein"| E["LSI bei Tabellenerstellung
definieren"] D -->|"Ja"| F["Tabelle neu erstellen
mit LSI oder GSI nutzen"] G --> H{"Provisioned oder
On-Demand?"} H -->|"Provisioned"| I["GSI-WCU/RCU separat
dimensionieren und überwachen"] H -->|"On-Demand"| J["GSI skaliert automatisch
mit Basistabelle"]

IAM-Berechtigungen für Index-Abfragen

Abfragen auf Sekundärindizes erfordern die dynamodb:Query-Berechtigung auf den Index-ARN, nicht nur auf die Tabelle. Ein häufiger Fehler: Die IAM-Policy erlaubt dynamodb:Query auf die Tabelle, aber der Index-ARN fehlt.

🔽 IAM-Policy: Zugriff auf Tabelle und GSI
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:Query",
        "dynamodb:GetItem"
      ],
      "Resource": [
        "arn:aws:dynamodb:us-east-1:123456789012:table/Bestellungen",
        "arn:aws:dynamodb:us-east-1:123456789012:table/Bestellungen/index/Status-Datum-GSI",
        "arn:aws:dynamodb:us-east-1:123456789012:table/Bestellungen/index/KundenId-Datum-LSI"
      ]
    }
  ]
}

LSI vs. GSI in DynamoDB: Zusammenfassung und nächste Schritte

Die Entscheidung zwischen LSI und GSI ist keine Geschmacksfrage — sie folgt aus den Zugriffsmustern. LSI, wenn Abfragen immer innerhalb des Partition Keys bleiben und starke Konsistenz benötigt wird. GSI, wenn tabellenübergreifende Abfragen nach beliebigen Attributen nötig sind und eventually consistent ausreicht.

Wer eine neue Tabelle entwirft, sollte alle Zugriffsmuster vor der Erstellung dokumentieren — LSIs lassen sich nachträglich nicht hinzufügen. GSIs sind flexibler, aber ihre Kapazität und Projektionsstrategie müssen aktiv überwacht werden.

Weiterführende Ressourcen:

Glossar

BegriffBedeutung
LSI (Local Secondary Index)Sekundärindex mit identischem Partition Key wie die Basistabelle, alternativer Sort Key. Nur bei Tabellenerstellung definierbar.
GSI (Global Secondary Index)Sekundärindex mit eigenem Partition Key. Jederzeit hinzufügbar, eigene Kapazität, nur eventually consistent.
ProjektionMenge der Attribute, die in den Index kopiert werden (KEYS_ONLY, INCLUDE, ALL).
Eventually ConsistentLeseoperation gibt möglicherweise veraltete Daten zurück, bis die Replikation abgeschlossen ist.
Hot PartitionEine DynamoDB-Partition, die überproportional viele Lese- oder Schreibanfragen erhält und dadurch gedrosselt wird.

Kommentare

Beliebte Posts aus diesem Blog

EC2 ohne Internetzugang im eigenen VPC – Internet Gateway und Route Table korrekt einrichten

ElastiCache Redis einsetzen: Wann ein Caching-Layer deine RDS-Datenbank entlastet