Principiante7 min de lectura

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 Scan lee la tabla entera, cada vez. El tamaño, no tu número de resultados, decide lo que pagas y cuánto tarda.
  • El FilterExpression miente 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 Scan se pone más lento conforme creces. Un Query por 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 Scan para 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, payloadBytes

Digamos 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 + filtroQuery por clave
LecturasCada item de la tablaUna partición, acotada por SK
Capacidad facturadaTabla entera, antes del filtroSolo los items de tu trozo
Nuestro ejemplo~250.000 RCUs (~2 GB)unos cientos de RCUs (~2 MB)
LatenciaCrece con el tamaño de la tablaPlana conforme crece la tabla
Nº de resultadosNo decide nada sobre el costeCoincide 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.

NoNoNecesitas leer items¿Sabes la clave de partición?Query una partición¿Puede un GSI clavearlo?Añade un GSI, luego QueryScan último recurso

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.

Actualizado