PostgreSQL
Normalización y desnormalización
Normalización mediante dependencias y formas normales, y desnormalización controlada cuando mejora lecturas sin abandonar una fuente de verdad clara.
- Última actualización
- Actualizada
- Nivel
- Aplicación
PostgreSQL
Normalización mediante dependencias y formas normales, y desnormalización controlada cuando mejora lecturas sin abandonar una fuente de verdad clara.
Normalizar significa separar hechos según sus dependencias para evitar anomalías. Desnormalizar significa duplicar o precalcular información deliberadamente, con una estrategia explícita para mantenerla consistente.
La normalización no consiste en “crear más tablas”. Consiste en ubicar cada dato donde depende de la clave correcta.
Modelo mental:
hecho
→ depende de una identidad
→ vive junto a esa identidadEjemplo:
customer_id → customer_emailEl email depende del customer, no de cada order. Repetirlo en todas las orders crea múltiples copias del mismo hecho.
Pero existen duplicaciones correctas. order_items.unit_price puede repetir el precio del producto porque representa un hecho histórico diferente: el precio acordado en esa venta.
Una tabla que mezcla customer y order:
order_id
order_date
customer_id
customer_name
customer_emailproduce anomalías.
No puedes registrar un customer hasta que exista una order.
Cambiar email exige modificar muchas filas. Si una queda atrás, existen varias “verdades”.
Eliminar la última order puede borrar la única copia del customer.
Separar customers y orders elimina esa dependencia incorrecta.
Una dependencia:
A → Bsignifica que un valor de A determina un único valor de B.
En products:
id → business_id, sku, name
(business_id, sku) → id, nameAmbas pueden ser candidate keys.
Las dependencias no se descubren solo mirando datos actuales. Que hoy dos customers no compartan email no prueba que email sea una key; el dominio debe garantizarlo.
1NF requiere que cada atributo contenga un valor perteneciente a su dominio y que la tabla tenga estructura relacional.
Error típico:
tag_list text -- 'food,priority,new'Problemas:
Alternativa:
CREATE TABLE product_tags (
product_id bigint NOT NULL REFERENCES products(id),
tag_id bigint NOT NULL REFERENCES tags(id),
PRIMARY KEY (product_id, tag_id)
);Un array puede seguir siendo atómico si el dominio trata el conjunto como una sola propiedad pequeña y no necesita identidad por elemento. 1NF no se reduce a “nunca arrays”; la decisión depende de semántica y operaciones.
Importa cuando existe una clave compuesta.
Tabla problemática:
order_id, product_id → quantity, unit_price, product_nameproduct_name depende solo de product_id, no de toda la clave (order_id, product_id).
Debe vivir en products. quantity y unit_price dependen de la combinación order-product y permanecen en order_items.
Evita dependencias transitivas entre atributos no clave.
Ejemplo:
order_id → customer_id
customer_id → customer_emailEntonces:
order_id → customer_emailes una dependencia transitiva. Guardar email actual en orders crea redundancia.
Pero si la columna representa billing_email_snapshot, ya no es el mismo atributo: es el email utilizado al emitir la factura. El nombre y propósito deben explicarlo.
Boyce-Codd Normal Form exige que todo determinante relevante sea candidate key.
Suele resolver casos donde 3NF todavía permite ciertas anomalías con varias candidate keys superpuestas.
No es necesario convertir cada diseño en ejercicio académico antes de crear una tabla, pero comprender determinantes ayuda cuando aparecen duplicados o reglas difíciles de expresar.
Diseño excesivo:
order_statuses(id, value)
customer_first_names(id, value)
boolean_values(id, value)Crear tablas para valores sin identidad ni comportamiento añade joins y navegación sin reducir una anomalía real.
Una tabla de catálogo se justifica cuando el concepto tiene:
CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
name text NOT NULL
);
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL REFERENCES customers(id),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
order_id bigint NOT NULL REFERENCES orders(id),
product_id bigint NOT NULL REFERENCES products(id),
quantity numeric(12,3) NOT NULL CHECK (quantity > 0),
unit_price numeric(14,2) NOT NULL CHECK (unit_price >= 0),
PRIMARY KEY (order_id, product_id)
);Responsabilidades:
Requisito:
Una factura debe conservar nombre, documento y dirección usados al emitirse, aunque el customer los cambie.
Opciones:
billing_name text NOT NULL,
billing_document text NOT NULL,
billing_address jsonb NOT NULLLa duplicación es intencional y representa historia.
customer
→ customer_profile_versions
→ invoice referencia una versiónAporta historial reutilizable y auditoría, pero aumenta complejidad.
Decisión:
Desnormalizar introduce una copia derivada para mejorar lectura o aislar workloads.
Ejemplos:
orders.total_amount derivado de items.La pregunta central es:
¿Cuál es la fuente de verdad?
¿Cómo y cuándo se actualiza la copia?
¿Qué staleness es aceptable?
¿Cómo se repara?CREATE TABLE orders (
id bigint PRIMARY KEY,
total_amount numeric(14,2) NOT NULL DEFAULT 0
);Si se mantiene manualmente:
insert/update/delete item
+
recalcular order total
+
misma transacciónRiesgos:
Alternativa: calcular al leer si el volumen lo permite.
Solo puede depender de columnas de la misma fila. No puede sumar filas de order_items.
Sirve para:
subtotal GENERATED ALWAYS AS (quantity * unit_price) STOREDNo para orders.total_amount basado en otra tabla.
CREATE MATERIALIZED VIEW daily_sales AS
SELECT date_trunc('day', paid_at) AS day,
sum(total_amount) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY 1;Ventajas:
Costes:
REFRESH ... CONCURRENTLY necesita unique index apropiado y tiene condiciones.En arquitecturas event-driven, un consumidor construye una tabla de lectura:
orders cambia
→ outbox event
→ worker actualiza dashboard_salesAporta independencia y escala de lectura. Introduce consistencia eventual, retries, idempotencia y reconstrucción.
No lo adoptes para evitar un join pequeño.
Antes de duplicar:
Una desnormalización puede acelerar una lectura y empeorar todas las escrituras.
PostgreSQL está diseñado para combinar relaciones. Un join bien indexado entre tablas razonables puede ser eficiente.
“Evitar joins” como regla conduce a documentos gigantes, datos repetidos y updates complejos.
El problema real puede ser:
Para valores pequeños, estables y sin metadata:
CHECK (status IN ('pending', 'paid', 'cancelled'))Para estados configurables o con propiedades:
CREATE TABLE order_statuses (
code text PRIMARY KEY,
label text NOT NULL,
is_terminal boolean NOT NULL
);Pero permitir estados configurables puede romper lógica de aplicación. No conviertas un state machine fijo en datos editables sin necesidad.
JSONB puede contener un documento autónomo:
provider_payload jsonbPero guardar customer, items y payment completos dentro de orders puede impedir:
Usa JSONB donde la forma es variable o el documento es una snapshot, no para evitar modelar hechos conocidos.
Duplicar tenant_name en cada order es inconsistencia si representa el nombre actual.
Sí puede ser correcto en una factura histórica.
Cada copia debe nombrar su semántica:
tenant_name_current → lookup
billing_name_snapshot → dato históricoUna copia puede quedar stale durante minutos y ser aceptable para analytics, no para stock disponible.
Dos writers actualizando contador derivado pueden perder incrementos. Usa operación atómica o recalculación.
Añadir una columna desnormalizada requiere poblarla y mantener dual-write durante transición.
Agregados deben definir si incluyen filas eliminadas.
Si se corrige un dato fraudulento, ¿deben cambiar snapshots emitidas? El dominio decide.
Se crean tablas pequeñas que no reducen anomalías.
Añade consistencia y mantenimiento sin beneficio comprobado.
Nadie sabe cómo repararlo.
La aplicación desconoce escrituras indirectas y el debugging se complica.
Los nombres ambiguos generan updates incorrectos.
Puede ser N+1 o índice ausente, no normalización.
INSERT, UPDATE, DELETE y upsert aplica cambios sobre el modelo y muestra cómo mantener atomicidad, concurrencia e integridad durante las escrituras.