aws.crafter.run

Datos

Single-table design

También llamado Single-table design

Guardar varios tipos de entidad en una misma tabla de DynamoDB con claves genéricas, de modo que una sola consulta devuelva todo lo que una pantalla necesita en vez de cinco.

DelicadoVerificado el

El problema

Vienes de SQL y modelas como te enseñaron: una tabla clientes, una pedidos, una direcciones, una lineas_pedido. En DynamoDB creas cuatro tablas y sigues adelante.

Entonces llega la pantalla de “detalle del cliente”, que necesita el perfil, sus últimos pedidos y sus direcciones. En SQL eso era un JOIN. Aquí son tres consultas, y como DynamoDB no tiene joins, las haces desde la Lambda:

const perfil      = await ddb.get({ TableName: 'clientes', Key: { id } });
const pedidos     = await ddb.query({ TableName: 'pedidos', ... });
const direcciones = await ddb.query({ TableName: 'direcciones', ... });

Cada una es rápida —cinco milisegundos— pero has convertido una pantalla en tres viajes de red que se suman, y en tres cosas que pueden fallar por separado. Cuando la pantalla crezca a cinco entidades, serán cinco.

Y hay un problema peor que la latencia: no puedes leerlas de forma consistente. Entre tu primera consulta y la tercera, otro proceso pudo cambiar los datos. En SQL el JOIN te daba una foto coherente; aquí tienes tres fotos tomadas en tres momentos.

La solución

Una sola tabla, con las claves primarias renombradas a algo deliberadamente genérico —PK y SK— y los distintos tipos de entidad conviviendo dentro, distinguidos por un prefijo en la clave.

PKSKTipoAtributos
CLIENTE#42PERFILPerfilnombre, email, alta
CLIENTE#42DIRECCION#casaDireccióncalle, ciudad, cp
CLIENTE#42PEDIDO#2026-09-01#1001Pedidototal, estado
CLIENTE#42PEDIDO#2026-09-03#1002Pedidototal, estado
PEDIDO#1001LINEA#1Líneasku, cantidad, precio
PEDIDO#1001LINEA#2Líneasku, cantidad, precio
Una sola Query con PK = CLIENTE#42 devuelve las cuatro filas resaltadas: el perfil, los dos pedidos y la dirección. Un viaje de red, una foto coherente. Las líneas del pedido viven bajo otra clave porque nadie las pide a la vez que el perfil.

Tres ideas hacen que esto funcione:

La colección de elementos. Todos los elementos que comparten PK se guardan físicamente juntos y ordenados por SK. Una Query sobre PK = CLIENTE#42 los devuelve todos de una vez, en un solo viaje. Eso es el sustituto del JOIN.

El prefijo en la clave de ordenación. Como el SK se ordena lexicográficamente, PERFIL, DIRECCION#… y PEDIDO#… quedan agrupados por tipo. Y como los pedidos llevan la fecha antes del id, salen ordenados por fecha sin hacer nada: SK begins_with "PEDIDO#2026-09" te da los de septiembre.

El GSI sobrecargado. Un índice con claves igual de genéricas —GSI1PK, GSI1SK— sirve para varios patrones de acceso a la vez, porque cada tipo de entidad mete ahí lo que necesita. Un solo GSI puede resolver “pedidos por estado” y “clientes por email” según qué haya escrito cada uno.

Cuándo usarlo

Cuándo NO usarlo

Cómo implementarlo

  1. Escribe primero la lista de patrones de acceso. Literalmente, en una tabla: “dado un cliente, traer su perfil y sus últimos 20 pedidos”. Si la lista está vacía, para aquí.
  2. Agrupa por lo que se lee junto. Lo que una pantalla pide a la vez quiere compartir PK.
  3. Diseña el SK para que ordene. Prefijo de tipo, y dentro, lo que quieras que salga en orden: fecha antes que id, casi siempre.
  4. Añade GSI sobrecargados solo para los patrones que la tabla base no cubre. Recuerda el techo: 20 GSI por tabla y 100 atributos proyectados sumando todos los índices.
  5. Proyecta lo justo. Cada GSI es una copia con su propio almacenamiento y su propia capacidad de escritura.
  6. Documenta el modelo. Una tabla de patrones de acceso a claves, en el repositorio. Sin ella, el siguiente que llegue hará un Scan.

El código

// Un unico Query devuelve el perfil, las direcciones y los pedidos.
const { Items } = await ddb.send(new QueryCommand({
  TableName: 'app',
  KeyConditionExpression: 'PK = :pk',
  ExpressionAttributeValues: { ':pk': `CLIENTE#${id}` },
}));

// El tipo se deduce del prefijo del SK: la tabla es generica,
// el codigo que la lee no tiene por que serlo.
const perfil = Items.find((i) => i.SK === 'PERFIL');
const direcciones = Items.filter((i) => i.SK.startsWith('DIRECCION#'));
const pedidos = Items.filter((i) => i.SK.startsWith('PEDIDO#'));

// Solo los pedidos de septiembre, ya ordenados por fecha porque
// la fecha va delante del id en el SK.
const septiembre = await ddb.send(new QueryCommand({
  TableName: 'app',
  KeyConditionExpression: 'PK = :pk AND begins_with(SK, :mes)',
  ExpressionAttributeValues: {
    ':pk': `CLIENTE#${id}`,
    ':mes': 'PEDIDO#2026-09',
  },
  ScanIndexForward: false, // los mas recientes primero
}));

Te va a morder

Coste

Single-table no es más barato por ser una tabla: DynamoDB factura por petición y por almacenamiento, no por número de tablas. Lo que ahorra es otra cosa:

Concepto Efecto
Consultas por pantalla de N a 1 — menos peticiones y menos latencia
Almacenamiento igual: los mismos datos ocupan lo mismo
GSI cada uno es una copia con su propia capacidad de escritura
Complejidad del equipo sube, y no aparece en la factura

El ahorro real está en el número de peticiones y en los viajes de red. Si tu pantalla hacía cinco consultas y ahora hace una, has dividido por cinco tanto el coste de lectura como la latencia acumulada.

Lo que sube es el coste humano. Es un intercambio legítimo cuando el volumen lo justifica, y un mal negocio cuando se hace por seguir una buena práctica que alguien leyó en un blog.

Fuentes

Tamaño máximo de elemento, límites de operaciones por lote y reparto de capacidad entre tabla e índices: Working with items and attributes in DynamoDB. Throughput por partición y el ejemplo del elemento de 20 KB: Best practices for designing and using partition keys. Cuotas de GSI, LSI y atributos proyectados, y la irreversibilidad de los LSI: Quotas in Amazon DynamoDB. Comportamiento del borrado por TTL: Using time to live (TTL) in DynamoDB.

Patrones relacionados

Un enlace sin la relación nombrada es un "ver también". Aquí cada uno dice qué relación tiene y por qué.