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 lautetSELECT … FROM … [WHERE …] [ORDER BY …]— und das ist die ganze Liste. KeinGROUP BY, keinHAVING, keine Aggregatfunktionen, keinJOIN(AWS-Referenz zu PartiQLSELECT). - Weil DynamoDB "doesn't natively support aggregation operations like
SUMorCOUNTacross 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/AVGdirekt 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):
- Die App schreibt das Raw-Item (eine Order, einen Download, ein Event). Keine Aggregationslogik.
- fängt den Write als Stream-Record ein.
- Eine Lambda am Stream liest das neue Item, leitet die Gruppe ab (Status,
Monat, Kategorie…) und
ADDt es mit einem atomarenUpdateItemauf das passende Aggregate-Item — was "avoids read-modify-write race conditions", wenn viele Invocations denselben Zähler anfassen. - Du fragst das vorberechnete Aggregat ab — oft über einen
, der nur die Rollup-Items indexiert, sodass
„Top 10 in diesem Monat“ eine einzige
QuerymitLimit 10ist.
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-Pfad | Muster 1 — atomarer Counter (ADD) |
| Aggregate ohne Write-Pfad | Muster 2 — Streams + Lambda |
| Count auf eine Partition | Muster 3 — Query, dann app-seitig |
| Exakte Totals | Muster 1/2 mit Idempotency-Guard |
Einmaliges GROUP BY explorativ | DynoTable Workbench oder Athena |
| Wiederkehrendes BI | Athena 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 DESCUnter 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.