Blog

Verbessern Sie die Leistung von DynamoDB-Abfragen mit Zusammenfassungsdaten

Jan Vermeir

Aktualisiert Oktober 15, 2025
5 Minuten

In diesem Blog haben wir uns eine einfache Lösung für die Verwendung von DynamoDB als Cache zum Speichern von Daten mit einer begrenzten Lebensdauer angesehen. In diesem Blog wurde gezeigt, wie man Werte speichert, die von einem Backend-Server abgerufen werden können. Dieser Dienst hat möglicherweise eine begrenzte Kapazität oder ist zu teuer, um viele Anfragen zu bearbeiten. Die Speicherung eines temporären Wertes kann helfen, die Last zu verringern. Außerdem ist DynamoDB besonders gut geeignet, um Daten aus einer möglicherweise sehr großen Tabelle per Primärschlüssel abzurufen.

Ein anderer Anwendungsfall ist etwas anders. Was ist, wenn wir viele Daten haben und nicht alle Daten abfragen möchten, nur um eine Zusammenfassung zu erhalten? Ein Beispiel wäre die Abfrage einer Gesamtsumme zu einem bestimmten Zeitpunkt auf der Grundlage einer Liste von Transaktionen. Wir haben vielleicht Tausende von Transaktionen für eine bestimmte Benutzer-ID und sind daran interessiert, die Summe aller Transaktionen sowie die letzten fünf anzuzeigen. In diesem Beitrag zeige ich Ihnen, wie Sie Zusammenfassungsdaten zu einer Tabelle hinzufügen können, um Abfragen zu beschleunigen.

Den Code für diesen Blog finden Sie hier: dynamodb/transaction unter main - jvermeir/dynamodb.

Die letzten Transaktionen aus einer Tabelle nach Eigentümer können mit GlobalSecondaryIndex abgerufen werden.

Dieser CDK-Code definiert eine Tabelle mit einem partitionKey:

    const transactionTable = new dynamoDB.Table(this, "TransactionTable", {
      tableName: "Transaction",
      partitionKey: {
        name: "id",
        type: dynamoDB.AttributeType.STRING,
      },
      billingMode: dynamoDB.BillingMode.PROVISIONED,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

Die Tabellendefinition zeigt einen Partitionsschlüssel, der auf einer ID basiert. Das kann alles Mögliche sein, z.B. eine generierte uuid. Fügen Sie einen sortKey wie diesen hinzu:

    transactionTable.addGlobalSecondaryIndex({
      indexName: "byOwnerByCreatedAt",
      partitionKey: { name: "owner", type: dynamoDB.AttributeType.STRING },
      sortKey: { name: "createdAt", type: dynamoDB.AttributeType.NUMBER },
      readCapacity: 1,
      writeCapacity: 1,
      projectionType: dynamoDB.ProjectionType.ALL,
    });

erstellt einen Index, der zur Abfrage von Daten nach dem Eigentümerfeld verwendet werden kann. Die Daten werden in sortierter Reihenfolge unter Verwendung eines in gespeicherten Zeitstempels gespeichert. Angesichts dieser Definition der Transaktionstabelle können wir diese Abfrage ausführen:

   let command = new QueryCommand({
      TableName: TRANSACTION_TABLE,
      IndexName: 'byOwnerByCreatedAt',
      ExpressionAttributeNames: {
        '#owner': 'owner',
        '#createdAt': 'createdAt',
      },
      ExpressionAttributeValues: {
        ':owner': owner,
        ':createdAt': timestamp,
      },
      KeyConditionExpression: '#owner = :owner AND #createdAt <= :createdAt',
      ScanIndexForward: false,
    });

Diese Abfrage würde alle Transaktionen für ein bestimmtes owner zurückgeben, die älter sind als ein timestamp, das als Parameter übergeben wird. ScanIndexForward: false, ist wichtig, weil es Datensätze abruft, die mit dem neuesten beginnen.

Mit dieser Abfrage durchläuft die Funktion getTotalByOwner in transaction/lambda/database.ts die Datensätze in einer Schleife und berechnet eine Summe, bis sie einen Zusammenfassungsdatensatz findet. Dann fügt sie einen neuen Zusammenfassungsdatensatz in die Transaktionstabelle ein, um Folgeabfragen zu beschleunigen. Die Daten in der Tabelle können wie folgt aussehen:

{
  "transactions": [
    {"amount": 14, "createdAt": 1690052471641, "owner": "owner1", "id": "id14"},
    {"amount": 91, "createdAt": 1690016544701, "owner": "owner1", "id": "summary-owner1-1690016544701", "type": "summary"},
    {"amount": 13, "createdAt": 1690016527772, "owner": "owner1", "id": "id13"},
    ...
    {"amount": 3, "createdAt": 1690016421753, "owner": "owner1", "id": "id3"},
    {"amount": 3, "createdAt": 1690016395422, "owner": "owner1", "id": "summary-owner1-1690016395422", "type": "summary"},
    {"amount": 2, "createdAt": 1690016368019, "owner": "owner1", "id": "id2"},
    {"amount": 1, "createdAt": 1690016361282, "owner": "owner1", "id": "id1"}
  ]
}

Es gibt also eine Liste von Transaktionen mit einem Betrag und einer ID, die nach createdAt sortiert sind und mit Transaktionen durchsetzt sind, bei denen type summary ist.

Der Algorithmus in const getTotalByOwner = async (owner: string)... ruft die Gesamtsumme ab und versucht dabei, mögliche Gleichzeitigkeitsprobleme in den Griff zu bekommen. In Pseudocode:

define the query
define a summary record with the current date as its createdAt value
while there's more data, get a set of record
    loop through the records
        add the value of amount to total
        exit the loop if the type of the record is summary
    update the query and get the next set of records
save the summary value in the transactions table        

Es gibt einige Fallstricke, die durch den Code in getTotalByOwner behoben werden. Es kann sein, dass mehrere Prozesse gleichzeitig versuchen, die Transaktionstabelle für eine owner zu lesen. Oder es kann ein Datensatz hinzugefügt werden, während wir die Zusammenfassung berechnen. Zum Schutz vor Gleichzeitigkeitsproblemen erstellt der Code als erste Aktion einen Zusammenfassungssatz mit dem aktuellen Datum als createdAt Wert. Dann werden Transaktionen abgefragt, die älter sind als der Zeitstempel im neuen Zusammenfassungsdatensatz. Dadurch wird der Datensatz, auf dem die Zusammenfassung basiert, eingefroren: Selbst wenn Datensätze hinzugefügt werden, ändert sich die für die Zusammenfassung berücksichtigte Menge nicht.

Aus Optimierungsgründen schreiben wir nicht jedes Mal, wenn die Transaktionen abgerufen werden, einen Zusammenfassungssatz. Die Datensätze werden von der Datenbank ohnehin in einem Satz zurückgegeben. Bevor eine Verarbeitung stattfinden kann, muss also ein vollständiger Satz geladen werden. Wir können also ein wenig Speicherplatz sparen, indem wir alle X Datensätze (10 im Codebeispiel) eine Zusammenfassung hinzufügen. Diese Strategie würde auch degenerierte Fälle vermeiden, in denen nach jedem Datensatz eine Zusammenfassung gespeichert wird.

Diese Optimierungen verkomplizieren den Code, aber wir könnten sie leicht in einer generischen Funktion verstecken.

Zusammenfassend lässt sich sagen, dass DynamoDB zum Zwischenspeichern von Daten verwendet werden kann, indem entweder Daten in einer separaten Tabelle oder Zusammenfassungen von Daten in derselben Tabelle gespeichert werden. Während die Cache-Tabelle für Daten nützlich ist, die von anderen Systemen abgerufen werden, ist die in diesem Blog beschriebene Technik der Zusammenfassungen eine gute Lösung, um den Abruf von Zusammenfassungen auf der Grundlage eines möglicherweise sehr großen Datensatzes zu beschleunigen.


Tags:

Verfasst von

Jan Vermeir

Developing software and infrastructure in teams, doing whatever it takes to get stable, safe and efficient systems in production.

Contact

Let’s discuss how we can support your journey.