Intermedio10 min de lectura

DynamoDB GROUP BY: cómo agregar de otra forma

No hay GROUP BY en DynamoDB. Tampoco hay COUNT, SUM ni AVG — ni en la API nativa, ni en PartiQL. DynamoDB es un almacén clave-valor / documental, no un motor analítico, así que la agregación es algo que construyes, no algo que hace el planificador de consultas por ti.

¿Puedes hacer GROUP BY en DynamoDB?

No. DynamoDB no tiene GROUP BY, HAVING ni funciones de agregación como COUNT, SUM y AVG — ni en la API nativa ni en PartiQL, cuyo SELECT solo acepta WHERE y ORDER BY. Agregas precalculando totales a medida que los datos cambian (contadores atómicos o resúmenes con + Lambda) o agrupando del lado de la aplicación después de leer.

  • La gramática de SELECT de PartiQL de DynamoDB es SELECT … FROM … [WHERE …] [ORDER BY …] — y esa es toda la lista. Sin GROUP BY, sin HAVING, sin funciones de agregación, sin JOIN (referencia de SELECT de PartiQL de AWS).
  • Como DynamoDB "no admite de forma nativa operaciones de agregación como SUM o COUNT a través de elementos", la propia guía de AWS es precalcular los agregados a medida que los datos cambian y almacenar los resultados como elementos corrientes (AWS: agregación materializada).
  • La alternativa — leer cada elemento y luego agregar en tu aplicación — funciona, pero pagas por leer toda la tabla en cada consulta.
  • Para exploración puntual, el SQL Workbench de DynoTable ejecuta GROUP BY / COUNT / SUM / AVG directamente contra una tabla en vivo — el SQL que el endpoint de PartiQL de DynamoDB rechaza.

Por qué la agregación es difícil en DynamoDB

DynamoDB no tiene motor de agregación en tiempo de escaneo. Query y Scan devuelven elementos; no los pliegan. Un Scan lee toda la tabla 1 MB a la vez, y la capacidad que consume se basa en los elementos que lee, no en las filas que conservas — una FilterExpression se aplica después del escaneo pero antes de que se devuelvan los resultados, así que estrecha el conjunto de resultados sin bajar la factura (referencia de la API Scan de AWS: un filtro "no consume unidades de capacidad de lectura adicionales"; la capacidad se basa en el tamaño del elemento escaneado, no devuelto). No hay un enganche de GROUP BY del que colgar una suma o un recuento en primer lugar.

PartiQL no cambia esto. PartiQL es un dialecto compatible con SQL sobre el mismo motor, así que hereda los mismos límites — es una superficie de sintaxis, no un nuevo modelo de ejecución. La gramática de SELECT documentada simplemente no tiene un token GROUP BY. Para la brecha completa entre PartiQL y el SQL real, consulta PartiQL frente a SQL.

Así que la pregunta no es "cómo escribo un GROUP BY" — es "dónde vive mi agregado y cuándo se calcula". Hay tres respuestas.

Patrón 1: agregar en la escritura (contadores atómicos)

Si conoces los grupos de antemano — recuento por estado, total por cliente, descargas por mes — mantén un elemento contador y actualízalo en cada escritura.

Usa una ADD para que el incremento sea atómico y seguro ante concurrencia. ADD funciona con números y conjuntos, y evita la carrera de leer-modificar-escribir, así que dos escritores incrementando el mismo contador nunca se pisan (AWS señala que el ADD atómico "evita las condiciones de carrera de leer-modificar-escribir"):

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

Este es tu SELECT COUNT(*) … GROUP BY status — salvo que el recuento ya está ahí como un elemento, legible en un GetItem de un solo dígito de milisegundos. La contrapartida: debes conocer la clave de agrupación en tiempo de escritura, y acoplas la actualización del contador a la ruta de escritura. Si la aplicación se cae después de la escritura pero antes de la actualización del contador, los dos se desincronizan — que es exactamente el modo de fallo que el siguiente patrón desacopla.

Patrón 2: resúmenes con DynamoDB Streams + Lambda

Cuando no quieres lógica de agregación en la ruta de escritura — o la escritura es un PutItem simple que no puedes envolver fácilmente — muévela aguas abajo. Este es el propio patrón recomendado de AWS, agregación materializada (AWS: Uso de GSI para consultas de agregación materializada):

  1. La aplicación escribe el elemento en bruto (un pedido, una descarga, un evento). Sin lógica de agregación.
  2. captura la escritura como un registro del stream.
  3. Una Lambda conectada al stream lee el nuevo elemento, deriva el grupo (estado, mes, categoría…) y hace ADD al elemento agregado correspondiente con un UpdateItem atómico — que "evita las condiciones de carrera de leer-modificar-escribir" cuando muchas invocaciones tocan el mismo contador.
  4. Consultas el agregado precalculado — a menudo a través de un que indexa solo los elementos de resumen, así que "top 10 de este mes" es una Query con Limit 10.

El truco del GSI disperso: solo los elementos agregados llevan el atributo indexado (p. ej. Month), así que las filas de eventos en bruto quedan excluidas del índice automáticamente — "una pequeña fracción del total de elementos de la tabla", lo que mantiene el índice barato y la lectura rápida.

Esto desacopla la agregación de la ruta de escritura y mantiene las escrituras simples, a costa de la coherencia eventual — AWS señala "un retraso de unos pocos segundos entre que una descarga se registra y que la agregación se actualiza". Para paneles, tablas de clasificación y contadores de tendencia eso está bien.

Aplica la misma advertencia de reintento: una invocación de Lambda reintentada vuelve a ejecutar el ADD, así que "un reintento incrementaría el recuento más de una vez", dejando un valor aproximado. Para recuentos exactos, añade idempotencia (p. ej. una expresión de condición con clave en el id del elemento fuente); de lo contrario, el pequeño margen está bien para análisis y tablas de clasificación.

Patrón 3: agrupación del lado de la aplicación tras Scan/Query

La opción de fuerza bruta: lee los elementos, agrúpalos en tu código.

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"])

Esto es correcto y a veces la decisión adecuada — pero sé honesto sobre el costo. Un Scan lee cada elemento de la tabla, y la capacidad de lectura es la misma filtres o no. Así que la agrupación del lado de la aplicación sobre un Scan completo significa que pagas por leer toda la tabla en cada agregación, y la latencia crece con la tabla. AWS enumera "escanear y contar en tiempo de lectura" como "solo adecuado para conjuntos de datos muy pequeños donde la latencia no es una preocupación" (AWS: Por qué precalcular agregaciones).

Acotado a una sola partición mediante Query (p. ej. contar los pedidos de un cliente), la agrupación del lado de la aplicación es perfectamente razonable — solo estás leyendo una colección de elementos. Para la brecha de costo completa entre las dos, consulta Query frente a Scan. En us-east-1 bajo demanda, un Scan de tabla completa factura 0,5 RCU por 4 KB con consistencia eventual por cada elemento examinado: una tabla de 1 GB con filas de 1 KB son unas 250.000 RCU antes de que tu aplicación agrupe nada. Dimensiona un elemento representativo con la calculadora de tamaño de elemento y después calcula la tarifa del escaneo en la calculadora de precios.

Para SQL analítico genuinamente ad hoc sobre una tabla de DynamoDB — el desechable "GROUP BY status, cuéntalos" que ejecutarías una vez— la respuesta de AWS es apuntar un motor separado a ella: el conector de Amazon Athena para DynamoDB te permite consultar la tabla con SQL real (GROUP BY, agregados, incluso JOIN a otras fuentes) mediante un conector Lambda (AWS: Conector de Amazon Athena para DynamoDB). Escanea la tabla entre bastidores, así que es una herramienta de informes/BI, no una ruta crítica.

¿Qué patrón uso?

Necesitas…Usa
Un total de grupo conocido en una ruta de lectura críticaPatrón 1 — contador atómico (ADD)
Agregados sin tocar la ruta de escrituraPatrón 2 — resumen con Streams + Lambda
Un recuento acotado a una particiónPatrón 3 — Query y luego agrupar en la aplicación
Totales exactos, sin desviaciónPatrón 1/2 con guarda de idempotencia
Un GROUP BY puntual mientras explorasWorkbench de DynoTable (abajo) o Athena
BI/informes recurrentes con SQLConector de Athena para DynamoDB

Ejecutar GROUP BY directamente en el SQL Workbench de DynoTable

Los patrones de arriba son cómo sirves agregados en producción. Pero cuando estás explorando una tabla — "¿cuántos pedidos por estado, ahora mismo?" — no quieres aprovisionar una Lambda ni levantar Athena. Quieres escribir la consulta.

Para eso está el SQL Workbench de DynoTable. Ejecuta SQL real — GROUP BY, COUNT, SUM, AVG, HAVING, incluso JOIN — directamente contra tus tablas de DynamoDB en vivo, ejecutando la agregación del lado del cliente sobre las filas que lee. Es el SQL que el endpoint de PartiQL de DynamoDB rechaza:

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

El planteamiento honesto: por debajo, DynoTable lee los elementos como la API lo permite (Query donde puede, Scan donde debe), los materializa y hace la agrupación en el Workbench — la misma mecánica de "leer y luego agregar" que el Patrón 3, solo que sin el bucle, y dentro de las reglas de patrones de acceso de DynamoDB. Está construido para exploración y análisis ad hoc, no para reemplazar un resumen de producción en una ruta de lectura crítica. Para eso, precalcula (Patrón 1 / 2).

Para el lado del JOIN de la misma cuña — DynoTable ejecuta uniones entre tablas que PartiQL tampoco puede — consulta DynamoDB JOIN. ¿Comparando clientes GUI en exactamente esta capacidad? Consulta la comparación de GUI de DynamoDB.

FAQ

¿Admite PartiQL de DynamoDB GROUP BY? No. El SELECT de PartiQL de DynamoDB admite solo WHERE y ORDER BY — sin GROUP BY, HAVING, funciones de agregación ni JOIN. La gramática está documentada como SELECT … FROM … [WHERE …] [ORDER BY …].

¿Puedo hacer COUNT(*) sobre toda una tabla de DynamoDB? No como función de agregación — PartiQL no tiene ninguna. La API te da Select=COUNT en un Scan/Query, que devuelve un recuento de elementos coincidentes pero aún lee (y factura) cada elemento que el escaneo toca (referencia de la API Scan de AWS: la capacidad se basa en los elementos examinados, no devueltos). Para un total leído con frecuencia, mantén un elemento contador (Patrón 1).

¿Puedo hacer GROUP BY de la clave de partición? No en DynamoDB ni en PartiQL. Si "por clave de partición" es un patrón de acceso conocido, mantén un elemento agregado por clave con un ADD atómico (Patrón 1), o resúmelo con Streams + Lambda (Patrón 2).

¿Cómo hago SUM o AVG por grupo? SUM: mantén un total acumulado por grupo y hazle ADD en la escritura. AVG: almacena tanto la suma como el recuento y divide en tiempo de lectura — no hay promedio nativo. Para un AVG exploratorio puntual, ejecútalo en el SQL Workbench de DynoTable o mediante el conector de Athena para DynamoDB.

¿Hay una solución alternativa de partiql group by? Ninguna del lado de PartiQL. O bien precalcula el agregado (contadores/Streams) y haz SELECT del elemento de resumen, o ejecuta el GROUP BY en un motor que tenga uno — el Workbench de DynoTable para ad hoc, Athena para informes recurrentes.


¿Quieres ejecutar GROUP BY contra tus propias tablas sin escribir una Lambda? Prueba DynoTable y apunta el SQL Workbench a una tabla en vivo.

Actualizado