Intermédiaire10 min de lecture

DynamoDB GROUP BY : comment agréger à la place

Il n'y a pas de GROUP BY dans DynamoDB. Il n'y a pas non plus de COUNT, SUM ou AVG — ni dans l'API native, ni dans PartiQL. DynamoDB est un store key-value / document, pas un moteur analytics, donc l'agrégation est quelque chose que tu construis, pas quelque chose que le query planner fait pour toi.

Peux-tu faire GROUP BY dans DynamoDB ?

Non. DynamoDB n'a ni GROUP BY, ni HAVING, ni de fonctions d'agrégat comme COUNT, SUM et AVG — ni dans l'API native ni dans PartiQL, dont le SELECT n'accepte que WHERE et ORDER BY. Tu agrèges en pré-computant les totaux quand les données changent (compteurs atomiques ou + Lambda rollups) ou en groupant côté app après la lecture.

  • La grammaire PartiQL SELECT de DynamoDB est SELECT … FROM … [WHERE …] [ORDER BY …] — et c'est toute la liste. Pas de GROUP BY, pas de HAVING, pas de fonctions d'agrégat, pas de JOIN (AWS PartiQL SELECT reference).
  • Parce que DynamoDB « doesn't natively support aggregation operations like SUM or COUNT across items », le guidance AWS lui-même est de pré-computer les agrégats quand les données changent et de stocker les résultats comme des items ordinaires (AWS : agrégation matérialisée).
  • L'alternative — lire chaque item puis agréger dans ton app — marche, mais tu paies pour lire tout le table à chaque query.
  • Pour l'exploration one-off, le SQL Workbench de DynoTable lance GROUP BY / COUNT / SUM / AVG directement contre un table live — le SQL que l'endpoint PartiQL de DynamoDB rejette.

Pourquoi l'agrégation est dure dans DynamoDB

DynamoDB n'a pas de moteur d'agrégation scan-time. Query et Scan renvoient des items ; ils ne les foldent pas. Un Scan lit tout le table 1 MB à la fois, et la capacité qu'il consomme est basée sur les items qu'il lit, pas les lignes que tu gardes — un FilterExpression est appliqué après le scan mais avant que les résultats reviennent, donc il réduit le result set sans baisser la facture (AWS Scan API reference : un filter « does not consume any additional read capacity units » ; la capacité est basée sur la taille d'item scannée, pas renvoyée). Il n'y a pas de hook GROUP-BY auquel accrocher un sum ou un count pour commencer.

PartiQL ne change pas ça. PartiQL est un dialecte SQL-compatible par-dessus le même moteur, donc il hérite des mêmes limites — c'est une surface de syntaxe, pas un nouveau modèle d'exécution. La grammaire SELECT documentée n'a simplement pas de token GROUP BY. Pour le gap complet entre PartiQL et le vrai SQL, vois PartiQL vs SQL.

Demande où vit ton agrégat et quand il est computé. Il y a trois réponses.

Pattern 1 : agréger à l'écriture (compteurs atomiques)

Si tu connais les groupes d'avance — count per status, total per customer, downloads per month — garde un item compteur et mets-le à jour à chaque écriture.

Utilise un ADD pour que l'incrément soit atomique et concurrency-safe. ADD marche sur numbers et sets, et évite la race read-modify-write, donc deux writers qui incrémentent le même compteur ne se clobber jamais (AWS note que le ADD atomique « avoids read-modify-write race conditions ») :

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

C'est ton SELECT COUNT(*) … GROUP BY status — sauf que le count est déjà assis là comme un item, lisible en un GetItem en millisecondes à un chiffre. Le trade-off : tu dois connaître la grouping key au write time, et tu couples l'update du compteur au write path. Si l'app crash après l'écriture mais avant l'update du compteur, les deux dérivent hors sync — exactement le failure mode que le pattern suivant découple.

Pattern 2 : DynamoDB Streams + Lambda rollups

Quand tu ne veux pas de logique d'agrégation sur le write path — ou que l'écriture est un plain PutItem que tu ne peux pas facilement wrapper — déplace-la downstream. C'est le pattern recommandé par AWS lui-même, materialized aggregation (AWS: Using GSIs for materialized aggregation queries) :

  1. L'app écrit l'item brut (un order, un download, un event). Pas de logique d'agrégation.
  2. capture l'écriture comme un stream record.
  3. Une Lambda attachée au stream lit le nouvel item, dérive le groupe (status, month, category…), et ADD vers l'item agrégat matching avec un UpdateItem atomique — qui « avoids read-modify-write race conditions » quand beaucoup d'invocations touchent le même compteur.
  4. Tu queries l'agrégat pré-computé — souvent via un qui indexe seulement les items rollup, donc « top 10 this month » est un Query avec Limit 10.

Seuls les items agrégat portent l'attribut indexé (par ex. Month), donc les lignes d'event brutes sont exclues de l'index automatiquement — « a small fraction of the total items in the table », ce qui garde l'index bon marché et la lecture rapide.

Ça découple l'agrégation du write path et garde les écritures simples, au prix de la cohérence à terme — AWS note « a delay of a few seconds between a download being recorded and the aggregation being updated ». Pour dashboards, leaderboards et compteurs de tendance, ça va.

Une invocation Lambda retried re-lance le ADD, donc « a retry would increment the count more than once », laissant une valeur approximative. Pour des counts exacts, ajoute de l'idempotency (par ex. un condition expression keyed sur l'id de l'item source) ; sinon la petite marge va bien pour analytics et leaderboards.

Pattern 3 : grouping côté app après Scan/Query

Ou lis les items et groupe-les dans ton 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"])

C'est correct et parfois le bon appel — mais sois honnête sur le coût. Un Scan lit chaque item du table, et la capacité de lecture est la même que tu filtres ou non. Donc le grouping côté app sur un Scan full veut dire que tu paies pour lire tout le table à chaque agrégation, et la latence grandit avec le table. AWS liste « scan and count at read time » comme « only suitable for very small datasets where latency isn't a concern » (AWS : pourquoi pré-calculer les agrégats).

Scopé à une seule partition via Query (par ex. count les orders d'un customer), le grouping côté app est parfaitement raisonnable — tu ne lis qu'une item collection. Pour le gap de coût complet entre les deux, vois Query vs Scan. En us-east-1 on-demand, un Scan full-table facture 0.5 RCU par 4 KB eventually-consistent pour chaque item examiné — un table de 1 GB de lignes de 1 KB fait environ 250 000 RCU avant que ton app groupe quoi que ce soit. Dimensionne un item représentatif avec le calculateur de taille d'item, puis line-rate le scan dans le calculateur de pricing.

Pour du vrai SQL analytique ad hoc sur un table DynamoDB — le throwaway « GROUP BY status, count them » que tu lancerais une fois — la réponse AWS est de pointer un moteur séparé dessus : le Amazon Athena DynamoDB connector te laisse query le table avec du vrai SQL (GROUP BY, agrégats, même des JOINs vers d'autres sources) via un connector Lambda (AWS : le connecteur Amazon Athena pour DynamoDB). Il scanne le table en coulisses, donc c'est un outil reporting/BI, pas un hot path.

Quel pattern j'utilise ?

Tu as besoin de…Utilise
Un total de groupe connu sur un hot read pathPattern 1 — compteur atomique (ADD)
Des agrégats sans toucher le write pathPattern 2 — Streams + Lambda rollup
Un count scopé à une partitionPattern 3 — Query puis group in app
Des totaux exacts, pas de driftPattern 1/2 avec guard d'idempotency
Un GROUP BY one-off pendant l'explorationDynoTable Workbench (ci-dessous) ou Athena
BI/reporting récurrent avec SQLAthena DynamoDB connector

Lancer GROUP BY directement dans le SQL Workbench de DynoTable

Les patterns ci-dessus sont comment tu sers des agrégats en production. Mais quand tu explores un table — « combien d'orders per status, right now ? » — tu ne veux pas provisionner une Lambda ou stand up Athena. Tu veux taper la query.

C'est à ça que sert le SQL Workbench de DynoTable. Il lance du vrai SQL — GROUP BY, COUNT, SUM, AVG, HAVING, même JOIN — directement contre tes tables DynamoDB live, exécutant l'agrégation côté client sur les lignes qu'il lit. C'est le SQL que l'endpoint PartiQL de DynamoDB rejette :

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

Sous le capot DynoTable lit les items comme l'API le permet (Query où elle peut, Scan où elle doit), les matérialise, et fait le grouping dans le Workbench — les mêmes mécaniques « read then aggregate » que le Pattern 3, juste sans la boucle, et dans les règles des modèles d'accès DynamoDB. C'est bâti pour l'exploration et l'analyse ad hoc, pas pour remplacer un rollup de production sur un hot read path. Pour ça, pré-compute (Pattern 1 / 2).

Pour le côté JOIN du même wedge — DynoTable lance des joins cross-table que PartiQL ne peut pas non plus — vois DynamoDB JOIN. Tu compares des clients GUI exactement sur cette capacité ? Vois la comparaison GUI DynamoDB.

FAQ

Est-ce que DynamoDB PartiQL support GROUP BY ? Non. Le SELECT PartiQL de DynamoDB support WHERE et ORDER BY seulement — pas de GROUP BY, HAVING, fonctions d'agrégat, ou JOIN. La grammaire est documentée comme SELECT … FROM … [WHERE …] [ORDER BY …].

Puis-je faire COUNT(*) sur tout un table DynamoDB ? Pas comme fonction d'agrégat — PartiQL n'en a aucune. L'API te donne Select=COUNT sur un Scan/Query, qui renvoie un count des items matchés mais lit (et facture) encore chaque item que le scan touche (AWS Scan API reference : la capacité est basée sur les items examinés, pas renvoyés). Pour un total lu fréquemment, garde un item compteur (Pattern 1).

Puis-je GROUP BY la partition key ? Pas dans DynamoDB ou PartiQL. Si « per partition key » est un modèle d'accès connu, maintain un item agrégat par clé avec un ADD atomique (Pattern 1), ou rollup avec Streams + Lambda (Pattern 2).

Comment faire SUM ou AVG per group ? SUM : garde un running total per group et ADD dessus à l'écriture. AVG : stocke à la fois le sum et le count et divise au read time — il n'y a pas de moyenne native. Pour un AVG exploratoire one-off, lance-le dans le SQL Workbench de DynoTable ou via le Athena DynamoDB connector.

Y a-t-il un workaround partiql group by ? Pas de côté PartiQL. Soit tu pré-computes l'agrégat (compteurs/Streams) et SELECT l'item rollup, soit tu lances le GROUP BY dans un moteur qui en a un — le Workbench de DynoTable pour l'ad hoc, Athena pour le reporting récurrent.


Envie de lancer GROUP BY contre tes propres tables sans écrire une Lambda ? Essaie DynoTable et pointe le SQL Workbench sur un table live.

Mis à jour