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:
| PK | SK | author | tags |
|---|---|---|---|
| POST#9f3 | META | {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:
| PK | SK | authorId | authorName | title |
|---|---|---|---|---|
| POST#9f3 | META | U#12 | "Mara Vance" | "Modeling 1:N" |
| POST#a71 | META | U#12 | "Mara Vance" | "Sparse GSIs" |
| POST#b04 | META | U#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) | |
|---|---|---|
| Forma | hijo anidado dentro del padre | el mismo valor en muchos items |
| Mejor para | datos acotados, propiedad del padre | un valor compartido que muestran muchos items |
| Lectura | un GetItem | un Query |
| Coste de update | reescribir el único item padre | fan-out a cada copia |
| Riesgo de tamaño | tope de item de 400 KB | ninguno 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:
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.