Intermedio7 min de lectura

Desnormalización en DynamoDB

Si vienes de SQL, la desnormalización suena a pecado — datos duplicados, sin una sola fuente de verdad. En DynamoDB es todo el punto. No hay joins, así que copias los datos relacionados al item que los necesita y los lees de vuelta de un solo tiro.

¿Qué es la desnormalización en DynamoDB?

Desnormalización en DynamoDB significa copiar datos relacionados al item que los lee, para que una sola query devuelva todo de un solo tiro. Como DynamoDB no tiene joins, pre-joins en el momento de escritura en lugar de coser tablas en el momento de lectura. El trade-off es staleness — solo duplica valores que cambian poco.

  • Sin joins significa que pre-joins en el momento de escritura. Guarda el valor relacionado en el item que lo lee, para que una query nunca necesite un segundo lookup.
  • Dos sabores. Embebe datos anidados en un atributo complejo de un solo item, o duplica un valor a lo largo de muchos items.
  • El pie es la staleness. Cuando cambia la fuente, cada copia está mal hasta que haces fan-out del update. Solo duplica valores que cambian poco.
  • Compra lecturas, no escrituras. Cambias más (y más cuidadosas) escrituras por lecturas baratas de una sola petición.

Por qué no hay joins a los que recurrir

Un JOIN relacional reensambla filas normalizadas en el momento de lectura. DynamoDB no tiene join — un Query lee una y devuelve exactamente lo que hay guardado ahí. Nada cose dos tablas por ti. (En el path de lectura de producción, al menos — para la auditoría ad-hoc o el check de drift, el SQL Workbench de DynoTable lanza un JOIN real sobre DynamoDB en el cliente.)

Así que los datos ya tienen que estar con forma para la lectura. Si una pantalla necesita un post y el nombre de su autor, ese nombre debe vivir en algún sitio que la lectura del post ya toque. El paper de Amazon Dynamo de 2007 hizo este trade explícito: soltar features relacionales para obtener lecturas previsibles a escala — el trade que DynamoDB entrega ahora como lecturas de un solo dígito de milisegundo.

Patrón 1 — embeber con un atributo complejo

Los atributos de DynamoDB pueden guardar maps y lists anidados, no solo escalares. Así que una forma común de desnormalización es meter un objeto hijo directamente dentro de su item padre en lugar de darle su propio item.

Un post con sus tags y un snapshot pequeño del autor, todo en un solo item:

PKSKauthortags
POST#9f3META{id: U#12, name: "Mara Vance"}["dynamodb","aws"]

Un solo GetItem devuelve el post, los tags y el bloque del autor juntos. Sin segunda lectura. Esto va genial para datos que son propiedad del padre y están acotados en tamaño — un puñado de tags, un snapshot de autor.

Un solo item de DynamoDB topea en 400 KB, nombres y valores de atributos incluidos (Service Quotas). Embebe una lista sin cota (cada comentario de un post viral) y te pasas.

Patrón 2 — duplicar un valor entre items

El caso del blog es el de libro. Listas posts y quieres que cada fila muestre el display name del autor — pero no quieres una segunda lectura por post para traerlo.

Así que escribes el nombre del autor en cada item de post cuando se crea el post:

PKSKauthorIdauthorNametitle
POST#9f3METAU#12"Mara Vance""Modeling 1:N"
POST#a71METAU#12"Mara Vance""Sparse GSIs"
POST#b04METAU#88"Lio Tan""Query vs Scan"

Un sobre posts (digamos GSI1PK = "POST", o uno claveado por autor) renderiza la lista entera — title y author — sin lookup por fila. begins_with sobre la clave de partición no es una cosa; un Query necesita igualdad de clave de partición, así que con claves de partición por post la lista viene del GSI, no de un Query sobre POST#. El nombre del autor está desnormalizado: la copia canónica vive en USER#12, y cada post lleva la suya.

El trade está ahí mismo. Has convertido una lectura N+1 en una sola lectura, a costa de guardar "Mara Vance" en N+1 sitios.

Embeber vs. duplicar — cuál

Embeber (atributo complejo)Duplicar (copiar entre items)
Formahijo anidado dentro del padreel mismo valor en muchos items
Mejor paradatos acotados, propiedad del padreun valor compartido que muestran muchos items
Lecturaun GetItemun Query
Coste de updatereescribir el único item padrefan-out a cada copia
Riesgo de tamañotope de item de 400 KBninguno por item

Ve a embeber cuando el hijo solo aparece con su padre. Ve a duplicar cuando muchos items independientes necesitan mostrar el mismo valor compartido.

El pie: copias stale

Mara se renombra a «Mara V.». Actualizas USER#12. Cada item de post sigue diciendo "Mara Vance" hasta que vas a arreglarlos.

Así que actualizar un valor duplicado es una escritura en fan-out, no una línea. Haces Query de cada item afectado y reescribes cada uno — idealmente protegido para tocar solo filas que aún llevan el valor viejo:

UPDATE POST#9f3
SET authorName = "Mara V."
WHERE authorName = "Mara Vance"

Puedes componer ese SET condicional contra authorName en el Expression Builder y copiar el UpdateExpression y el ConditionExpression generados directo a tu código.

El fan-out es una escritura por item. Haz Query al GSI claveado por autor para los posts de ese autor, luego lanza los updates. La secuencia:

"DynamoDB"App"DynamoDB"App"Update nombre de USER"Query posts del autor""POST"Update de cada authorName"

Cada cambio de la fuente es un Query más una escritura por copia. En DynoTable el fan-out aterriza primero en el área de staging, como un diff revisable por atributo por item — ves cada copia que está a punto de cambiar antes de que salga nada.

Por eso la regla es solo duplica valores que cambian poco. Un display name, un plan tier, una etiqueta de categoría — bien. Un contador en vivo o un campo editado a menudo — no; el fan-out te come vivo.

Coste de escritura del fan-out

Cada copia que actualizas es una escritura facturada aparte. En on-demand de us-east-1, actualizar 50 items de post tras un rename de autor cuesta 50 × WCU — normalmente 1 WCU por KB por item cuando cada fila de post es ≤ 1 KB. El lado de lectura se queda en un Query; el de escritura escala con cuántos duplicados mantienes. Estima ambos paths en la calculadora de precios.

Cuándo la normalización sigue ganando

Si un valor cambia a menudo, o un item se lee con patrones de verdad impredecibles, mantenlo normalizado y acepta la lectura extra. La desnormalización es una optimización para access patterns conocidos y read-heavy — no un default para aplicar en todas partes. Pre-join las lecturas que de verdad lanzas, y deja el resto en paz.

Para decidir dónde viven estos atributos duplicados, modela primero los access patterns — ver single-table design y, para el lado de lectura del trade, Query vs Scan.

Descarga DynoTable para inspeccionar una tabla desnormalizada, detectar qué copias se han desviado y lanzar el update en fan-out contra tus propios datos. Su SQL Workbench puede incluso hacer JOIN de la fuente de verdad contra las copias para encontrar el drift en una sola query.

Actualizado