Fortgeschritten8 Min. Lesezeit

DynamoDB GROUP BY: Stattdessen aggregieren

Es gibt kein GROUP BY in DynamoDB. Auch kein COUNT, SUM oder AVG — weder in der nativen API noch in PartiQL. DynamoDB ist ein Key-Value- / Document-Store, keine Analytics-Engine: Aggregation ist deshalb etwas, das du baust, nicht etwas, das dir der Query-Planner abnimmt.

Kann man in DynamoDB GROUP BY?

Nein. DynamoDB hat kein GROUP BY, kein HAVING und keine Aggregatfunktionen wie COUNT, SUM und AVG — weder in der nativen API noch in PartiQL, dessen SELECT nur WHERE und ORDER BY akzeptiert. Du aggregierst, indem du Totals vorberechnest, während sich die Daten ändern (atomare Zähler oder + Lambda-Rollups), oder indem du nach dem Lesen app-seitig gruppierst.

  • Die PartiQL-SELECT-Grammatik von DynamoDB lautet SELECT … FROM … [WHERE …] [ORDER BY …] — und das ist die ganze Liste. Kein GROUP BY, kein HAVING, keine Aggregatfunktionen, kein JOIN (AWS-Referenz zu PartiQL SELECT).
  • Weil DynamoDB "doesn't natively support aggregation operations like SUM or COUNT across items," lautet AWS' eigene Empfehlung, Aggregate vorzuberechnen, während sich die Daten ändern, und die Ergebnisse als ganz normale Items zu speichern (AWS: materialized aggregation).
  • Die Alternative — jedes Item lesen und dann in deiner App aggregieren — funktioniert, aber du bezahlst bei jeder Query dafür, die ganze Tabelle zu lesen.
  • Für einmalige Exploration führt DynoTables SQL Workbench GROUP BY / COUNT / SUM / AVG direkt gegen eine Live-Tabelle aus — genau das SQL, das DynamoDBs PartiQL-Endpoint ablehnt.

Warum Aggregation in DynamoDB schwer ist

DynamoDB hat keine Aggregations-Engine zur Scan-Zeit. Query und Scan liefern Items; sie falten sie nicht. Ein Scan liest die ganze Tabelle in 1-MB-Schritten, und die Capacity, die er verbraucht, richtet sich nach den gelesenen Items, nicht nach den Zeilen, die du behältst — eine FilterExpression wird nach dem Scan, aber vor der Rückgabe angewandt und verengt so das Result-Set, ohne die Rechnung zu senken (AWS-Scan-API-Referenz: ein Filter "does not consume any additional read capacity units"; die Capacity richtet sich nach der gescannten, nicht nach der zurückgegebenen Item-Größe). Es gibt überhaupt keinen GROUP-BY-Hook, an den du eine Summe oder einen Count hängen könntest.

PartiQL ändert daran nichts. PartiQL ist ein SQL-kompatibler Dialekt über derselben Engine und erbt deshalb dieselben Limits — eine Syntax-Oberfläche, kein neues Ausführungsmodell. Die dokumentierte SELECT-Grammatik kennt schlicht kein GROUP BY-Token. Zur vollen Lücke zwischen PartiQL und echtem SQL siehe PartiQL vs SQL.

Frag dich, wo dein Aggregat lebt und wann es berechnet wird. Darauf gibt es drei Antworten.

Muster 1: Bei Write aggregieren (atomare Zähler)

Kennst du die Gruppen im Voraus — Anzahl pro Status, Total pro Kunde, Downloads pro Monat —, dann halte ein Counter-Item und aktualisiere es bei jedem Write.

Nimm dafür eine ADD-, damit das Inkrement atomar und nebenläufigkeitssicher ist. ADD arbeitet auf Zahlen und Sets und umgeht den Read-Modify-Write-Race, sodass zwei Writer, die denselben Zähler hochzählen, sich nie gegenseitig überschreiben (AWS zum atomaren ADD: "avoids read-modify-write race conditions"):

UpdateItem
Key                         { pk: "STATS#orders", sk: "status#shipped" }
UpdateExpression            "ADD orderCount :one"
ExpressionAttributeValues   { ":one": 1 }

Das ist dein SELECT COUNT(*) … GROUP BY status — nur dass der Count schon als Item bereitliegt und per GetItem in einstelligen Millisekunden lesbar ist. Der Trade-off: Du musst den Grouping-Key zur Write-Zeit kennen, und du koppelst das Counter-Update an den Write-Pfad. Stürzt die App nach dem Write, aber vor dem Counter-Update ab, driften die beiden auseinander — und genau diesen Fehlerfall entkoppelt das nächste Muster.

Muster 2: DynamoDB Streams + Lambda-Rollups

Wenn du keine Aggregationslogik auf dem Write-Pfad willst — oder der Write ein schlichtes PutItem ist, das du nicht ohne Weiteres umschließen kannst —, verschieb sie nach hinten. Das ist AWS' eigenes empfohlenes Muster, die materialized aggregation (AWS: Using GSIs for materialized aggregation queries):

  1. Die App schreibt das Raw-Item (eine Order, einen Download, ein Event). Keine Aggregationslogik.
  2. fängt den Write als Stream-Record ein.
  3. Eine Lambda am Stream liest das neue Item, leitet die Gruppe ab (Status, Monat, Kategorie…) und ADDt es mit einem atomaren UpdateItem auf das passende Aggregate-Item — was "avoids read-modify-write race conditions", wenn viele Invocations denselben Zähler anfassen.
  4. Du fragst das vorberechnete Aggregat ab — oft über einen , der nur die Rollup-Items indexiert, sodass „Top 10 in diesem Monat“ eine einzige Query mit Limit 10 ist.

Nur die Aggregate-Items tragen das indexierte Attribut (etwa Month), also bleiben die Raw-Event-Zeilen automatisch aus dem Index draußen — "a small fraction of the total items in the table," was den Index billig und den Read schnell hält.

Das entkoppelt die Aggregation vom Write-Pfad und hält Writes simpel, zum Preis von Eventual Consistency — AWS spricht von "a delay of a few seconds between a download being recorded and the aggregation being updated." Für Dashboards, Leaderboards und Trend-Zähler ist das in Ordnung.

Eine erneut ausgeführte Lambda-Invocation wiederholt das ADD, es gilt also "a retry would increment the count more than once," und der Wert bleibt ein Näherungswert. Für exakte Counts ergänzt du Idempotenz (etwa eine Condition Expression auf der ID des Quell-Items); sonst ist die kleine Abweichung für Analytics und Leaderboards unproblematisch.

Muster 3: App-seitiges Grouping nach Scan/Query

Oder du liest die Items und gruppierst sie in deinem Code.

groups = {}
resp = table.scan()                        # or query() for one partition
while True:
    for item in resp["Items"]:
        key = item["status"]
        groups[key] = groups.get(key, 0) + 1
    if "LastEvaluatedKey" not in resp:
        break
    resp = table.scan(ExclusiveStartKey=resp["LastEvaluatedKey"])

Das ist korrekt und manchmal die richtige Wahl — aber sei ehrlich zu den Kosten. Ein Scan liest jedes Item in der Tabelle, und die Read Capacity ist dieselbe, ob du filterst oder nicht. App-seitiges Grouping über einen vollen Scan heißt also: Du bezahlst bei jeder Aggregation dafür, die gesamte Tabelle zu lesen, und die Latenz wächst mit der Tabelle. AWS bezeichnet "scan and count at read time" als "only suitable for very small datasets where latency isn't a concern" (AWS: Why pre-compute aggregations).

Auf eine einzelne Partition per Query eingegrenzt (etwa die Orders eines Kunden zählen), ist App-seitiges Grouping dagegen völlig vernünftig — du liest nur eine Item Collection. Zum vollen Kostenunterschied zwischen beiden siehe Query vs Scan. On-Demand in us-east-1 berechnet ein Full-Table-Scan 0,5 RCU pro 4 KB eventually consistent für jedes untersuchte Item — eine 1‑GB-Tabelle mit 1‑KB-Zeilen sind grob 250.000 RCU, bevor deine App überhaupt gruppiert. Miss ein repräsentatives Item im Item-Size-Calculator und rechne den Scan dann im Pricing Calculator durch.

Für wirklich ad-hoc analytisches SQL über eine DynamoDB-Tabelle — das Wegwerf-„GROUP BY status, dann zählen“, das du genau einmal laufen lässt — lautet AWS' Antwort, eine separate Engine darauf zu richten: Der Amazon Athena DynamoDB Connector lässt dich die Tabelle über einen Lambda-Connector mit echtem SQL abfragen (GROUP BY, Aggregate, sogar JOINs auf andere Quellen) (AWS: Amazon Athena DynamoDB connector). Er scannt die Tabelle im Hintergrund — also ein Reporting-/BI-Werkzeug, kein Hot Path.

Welches Muster wann?

Du brauchst…Nutze
Bekanntes Total auf heißem Read-PfadMuster 1 — atomarer Counter (ADD)
Aggregate ohne Write-PfadMuster 2 — Streams + Lambda
Count auf eine PartitionMuster 3 — Query, dann app-seitig
Exakte TotalsMuster 1/2 mit Idempotency-Guard
Einmaliges GROUP BY explorativDynoTable Workbench oder Athena
Wiederkehrendes BIAthena DynamoDB Connector

GROUP BY direkt in DynoTables SQL Workbench

Die Muster oben sind der Weg, Aggregate in Produktion auszuliefern. Aber wenn du eine Tabelle explorierst — „wie viele Orders pro Status, genau jetzt?“ —, willst du keine Lambda provisionieren und kein Athena aufsetzen. Du willst die Query tippen.

Genau dafür ist DynoTables SQL Workbench da. Er führt echtes SQL aus — GROUP BY, COUNT, SUM, AVG, HAVING, sogar JOIN — direkt gegen deine Live-DynamoDB-Tabellen und rechnet die Aggregation client-seitig über den gelesenen Zeilen. Es ist das SQL, das DynamoDBs PartiQL-Endpoint ablehnt:

SELECT status, COUNT(*) AS orders, SUM(total) AS revenue
FROM "Orders"
GROUP BY status
HAVING SUM(total) > 1000
ORDER BY revenue DESC

Unter der Haube liest DynoTable Items so, wie die API es erlaubt (Query, wo es geht, Scan, wo es sein muss), materialisiert sie und gruppiert im Workbench — dieselbe „erst lesen, dann aggregieren“-Mechanik wie in Muster 3, nur ohne die Schleife, und innerhalb von DynamoDBs Access-Pattern-Regeln. Gebaut ist er für Exploration und Ad-hoc-Analyse, nicht dafür, einen Production-Rollup auf einem heißen Read-Pfad zu ersetzen. Dafür: vorberechnen (Muster 1 / 2).

Zur JOIN-Seite desselben Keils — DynoTable fährt Cross-Table-Joins, die PartiQL ebenso wenig kann — siehe DynamoDB JOIN. Wenn du GUI-Clients genau auf diese Fähigkeit hin vergleichst, siehe den DynamoDB-GUI-Vergleich.

FAQ

Unterstützt DynamoDBs PartiQL GROUP BY? Nein. DynamoDBs PartiQL-SELECT unterstützt nur WHERE und ORDER BY — kein GROUP BY, kein HAVING, keine Aggregatfunktionen, kein JOIN. Die Grammatik ist als SELECT … FROM … [WHERE …] [ORDER BY …] dokumentiert.

Kann ich COUNT(*) über eine ganze DynamoDB-Tabelle laufen lassen? Nicht als Aggregatfunktion — PartiQL hat keine. Die API gibt dir Select=COUNT auf Scan/Query; das liefert einen Count der gematchten Items, liest (und berechnet) aber weiterhin jedes Item, das der Scan berührt (AWS-Scan-API-Referenz: die Capacity richtet sich nach untersuchten, nicht nach zurückgegebenen Items). Für ein häufig gelesenes Total hältst du ein Counter-Item (Muster 1).

Kann ich nach dem Partition Key GROUP BY machen? Nicht in DynamoDB und nicht in PartiQL. Ist „pro Partition Key“ ein bekanntes Access Pattern, pflegst du ein Aggregate-Item pro Key mit einem atomaren ADD (Muster 1) oder rollst es mit Streams + Lambda hoch (Muster 2).

Wie mache ich SUM oder AVG pro Gruppe? SUM: ein laufendes Total pro Gruppe halten und beim Write ADDen. AVG: Summe und Count beide speichern und zur Read-Zeit dividieren — einen nativen Durchschnitt gibt es nicht. Für ein einmaliges exploratives AVG führst du es in DynoTables SQL Workbench oder über den Athena-DynamoDB-Connector aus.

Gibt es einen partiql group by-Workaround? Keinen auf PartiQL-Seite. Entweder du berechnest das Aggregat vor (Counter/Streams) und SELECTest das Rollup-Item, oder du fährst das GROUP BY in einer Engine, die eines hat — DynoTables Workbench für ad hoc, Athena für wiederkehrendes Reporting.


Willst du GROUP BY gegen deine eigenen Tabellen fahren, ohne eine Lambda zu schreiben? DynoTable ausprobieren und den SQL Workbench auf eine Live-Tabelle richten.

Aktualisiert