Por qué un Scan de DynamoDB es lento y caro
Un Scan lee cada item de la tabla y solo filtra después. Es la operación
a la que llegas por memoria muscular de SQL, y la que en silencio infla tu
factura mientras empeora tu latencia más que la caja de RDS que dejaste.
¿Por qué mi Scan de DynamoDB es lento y caro?
Un Scan lee cada item de la tabla antes de que corra el FilterExpression, así
que pagas por leer la tabla entera da igual cuántas filas vuelvan, y se pone más
lento conforme crece la tabla. El arreglo casi siempre es un Query por clave —
modela el access pattern alrededor de una clave para que DynamoDB toque una
partición en lugar de todo.
- Un
Scanlee la tabla entera, cada vez. El tamaño, no tu número de resultados, decide lo que pagas y cuánto tarda. - El
FilterExpressionmiente sobre el coste. Corre después de que la lectura está medida, así que devolver 12 items puede facturar la lectura de 12 millones. - Un
Scanse pone más lento conforme creces. UnQuerypor clave se queda plano — toca una partición da igual lo grande que se ponga la tabla. - El arreglo casi siempre es modelado, no tuning. Si haces
Scanpara responder una pregunta rutinaria, te falta una clave.
Qué hace de verdad un Scan
Si vienes de SQL, SELECT * FROM events WHERE type = 'checkout' se siente
gratis — el motor tiene un índice, o no, pero de cualquier forma te devuelve
filas. En DynamoDB no hay un query planner que decida eso por ti.
Un Scan recorre la tabla entera en secuencia, 1 MB cada vez, y pasa cada
página a tu FilterExpression. Lo que el filtro rechaza sigue leído, sigue
medido y sigue en tu factura. (AWS: Scanning tables)
Esa es la trampa. El filtro parece una cláusula WHERE, pero cambia el result
set, nunca el coste. Un Scan consume la misma capacidad de lectura esté o no
presente un filtro. (AWS: Scanning tables)
Cuenta las unidades de lectura
DynamoDB mide las lecturas en (RCUs). Un RCU compra una sola lectura de un item de hasta 4 KB; las lecturas de cuestan la mitad. Los items más grandes redondean al siguiente 4 KB. (AWS: Read/write capacity mode)
Toma una tabla de analytics, ProductEvents. Cada fila es un evento trackeado:
PK = "TENANT#acme"
SK = "TS#2026-06-23T14:08:55Z#evt_9f3a"
attrs: eventType, sessionId, userId, payloadBytesDigamos que guarda 2.000.000 de eventos, cada uno ~1 KB, todos bajo un tenant ocupado. Quieres los checkouts de hoy. El movimiento reflejo:
Scan ProductEvents
FilterExpression: eventType = "checkout"
Ese filtro puede devolver 40 filas. Pero el Scan leyó primero los 2.000.000
de items. A ~1 KB cada uno (1 RCU por 4 KB, consistencia eventual ≈ 0,5 RCU por
4 KB), mediste más o menos 250.000 RCUs — y paginaste ~2 GB de datos — para
devolver 40 items.
Ahora modela el access pattern como una clave y haz Query en su lugar:
Query ProductEvents
PK = "TENANT#acme"
AND SK begins_with "TS#2026-06-23"
Esto lee solo el trozo matchado de una partición. Si esas 40 filas de checkout más los otros eventos del día suman ~2 MB, pagas por ~2 MB de lecturas, no 2 GB. Misma respuesta, una fracción minúscula del coste — y la latencia se queda plana conforme crece la tabla.
Scan vs Query, medidos
| Scan + filtro | Query por clave | |
|---|---|---|
| Lecturas | Cada item de la tabla | Una partición, acotada por SK |
| Capacidad facturada | Tabla entera, antes del filtro | Solo los items de tu trozo |
| Nuestro ejemplo | ~250.000 RCUs (~2 GB) | unos cientos de RCUs (~2 MB) |
| Latencia | Crece con el tamaño de la tabla | Plana conforme crece la tabla |
| Nº de resultados | No decide nada sobre el coste | Coincide con lo que pagas |
En un Scan, tu número de resultados y tu factura no están relacionados. En un
Query, se siguen el uno al otro.
Decide antes de hacer Scan
La mayoría de los Scans accidentales vienen de una pregunta: ¿puedo nombrar
la partición que necesito? Si sí, es un Query. Si no, el arreglo es una
clave, no un filtro más gordo.
El path casi siempre termina en Query; solo caes a Scan cuando ninguna
clave — presente o añadible — encaja con el access pattern.
Si el patrón es real y recurrente pero la tabla base no puede clavearlo, esa es
la señal para añadir un Global Secondary Index para
que la pregunta se convierta en un Query. Modelar tus claves alrededor de tus
access patterns de antemano es todo el juego — ver
single-table design.
Escribe el Query por clave, no un filtro
Cuando de verdad necesitas una condición más allá de la clave, constrúyela a
propósito en lugar de tirar todo a un FilterExpression. El
DynamoDB Expression Builder genera el
KeyConditionExpression y los placeholders de atributos por ti, para que la
clave de partición y la de ordenación hagan el acotado — antes de que DynamoDB
mida la lectura, no después.
KeyConditionExpression: PK = :tenant AND begins_with(SK, :day)
Cuándo un Scan está bien de verdad
Un Scan es el default equivocado para queries rutinarias. Es la herramienta
correcta cuando de verdad quieres decir «lee todo»:
- Exports one-off o backfills lanzados a mano.
- Tablas tiny de config / lookup donde la tabla entera son unos KB.
- Jobs de background que paginan la tabla completa a propósito. Partelos
entre workers con
Segment/TotalSegments— un — en lugar de un rastreo secuencial largo. (AWS: Scanning tables)
Y nota que PartiQL no te salva: SELECT * FROM ProductEvents WHERE eventType = 'checkout' sin predicado de clave compila directo a un Scan. Es
el mismo pie con ropa de SQL. (Ver Query vs Scan
para el desglose completo.)
Cuando de verdad necesitas analytics entre items — un GROUP BY, un JOIN, un
agregado que DynamoDB no puede expresar — el SQL Workbench de DynoTable los
ejecuta en el cliente sobre un result set acotado, en lugar de martillar la
tabla con un Scan completo.
Próximos pasos
Estima lo que cuesta cualquiera de los dos patrones con la calculadora de precios, lee Query vs Scan para el contraste a nivel de API, y descarga DynoTable para lanzarlos contra tus propias tablas y ver cuántos items lee de verdad cada enfoque.