Normalización

"Normalizar es como ordenar la cocina: parece innecesario hasta que necesitás encontrar algo rápido."

Qué vas a aprender en este capítulo

La normalización es un proceso formal para diseñar bases de datos que eviten redundancia y anomalías. Vas a aprender qué son las dependencias funcionales, las formas normales clásicas (1FN, 2FN, 3FN, BCNF), cómo descomponer tablas mal diseñadas, y cuándo desnormalizar a propósito por razones de rendimiento. Al final del capítulo cerrás el libro con un esquema rigurosamente correcto para la pupusería.

4.1 La idea: por qué importa

💡 Intuición

Tabla mal diseñada (todo junto):

pedido_id cliente_nombre cliente_email producto precio cantidad
1 María maria@x.com Pupusa de queso 0.50 5
1 María maria@x.com Refresco 1.00 2
2 Juan juan@x.com Pupusa revuelta 0.60 3
3 María maria@nuevo.com Pupusa de queso 0.50 2

Problemas:

  1. Redundancia. "María" y su email aparecen en cada línea de cada uno de sus pedidos.
  2. Anomalía de actualización. Si María cambia email (ya pasó: pedido 3), los pedidos viejos quedan desactualizados. ¿Cuál es su email "real"?
  3. Anomalía de inserción. ¿Cómo agregás un cliente nuevo si todavía no compró nada? Necesitás un pedido vacío.
  4. Anomalía de eliminación. Si borrás el único pedido de Juan, perdés también su info de cliente.

La normalización es un proceso para descomponer tablas mal diseñadas en varias bien diseñadas, eliminando estas anomalías.

Resultado:

  • Tabla cliente (nombre, email).
  • Tabla producto (nombre, precio).
  • Tabla pedido (id, cliente_id).
  • Tabla pedido_linea (pedido_id, producto_id, cantidad).

Cada dato vive en un solo lugar. Cambiarlo es modificar una fila.

4.2 Dependencias funcionales

📐 Fundamento

Una dependencia funcional (DF) describe cómo un valor determina otro.

Notación: XYX \rightarrow Y significa "X determina Y" — si conocés X, conocés Y de manera única.

Ejemplos:

  • matriculanombre\text{matricula} \rightarrow \text{nombre} : una matrícula identifica únicamente al alumno.
  • ISBNtitulo,autor\text{ISBN} \rightarrow \text{titulo}, \text{autor} : el ISBN determina título y autor.
  • producto_idprecio\text{producto_id} \rightarrow \text{precio} : cada producto tiene un precio.

Importante: las DF reflejan reglas del dominio del problema, no de los datos actuales. Aunque ahora dos productos casualmente tengan el mismo precio, no significa que precio dependa solo de producto_id (la regla puede cambiar).

Tipos:

  • DF trivial. AAA \rightarrow A. Cualquier atributo se determina a sí mismo. No aporta información.
  • DF parcial. ABA \rightarrow B donde A es parte de la PK. Problema en 2FN.
  • DF transitiva. ABCA \rightarrow B \rightarrow C, donde B no es clave. Problema en 3FN.

Cómo encontrarlas: preguntale al dominio del negocio. "Si te doy el ISBN, ¿podés decirme título y autor sin ambigüedad?" Si sí, hay DF.

Cierre. El conjunto de todas las DFs implicadas por un conjunto dado se llama cierre (closure). Hay axiomas (Armstrong) para derivarlo. Lo dejamos como avanzado.

4.3 Primera Forma Normal (1FN)

📐 Fundamento

Regla: cada celda contiene un solo valor atómico. Sin listas, sin estructuras anidadas.

Mal:

cliente telefonos
María "55-12, 55-34, 55-56"

Bien:

cliente telefono
María 55-12
María 55-34
María 55-56

(Y mejor todavía: tabla aparte cliente_telefono.)

Indicios de violación:

  • Listas o sets en una sola celda ("rojo,azul,verde").
  • Campos numerados (telefono1, telefono2, telefono3 — semi-mal).
  • JSON o XML embebido cuando los datos son consultables.

Cuando JSON sí es OK: datos opacos para la base, no consultables. Por ejemplo, configuración del usuario que solo el cliente parsea. Si vas a hacer WHERE config->'theme' = 'dark', ya no es opaco — debería ser tabla aparte o columna.

4.4 Segunda Forma Normal (2FN)

📐 Fundamento

Pre-requisito: la tabla está en 1FN.

Regla: ningún atributo no-clave depende de parte de la PK. (Solo aplica si la PK es compuesta.)

Ejemplo de violación:

Tabla pedido_linea:
PK = (pedido_id, producto_id)

Atributos:
  pedido_id, producto_id, cantidad, importe, nombre_producto, precio_actual_producto
                                              ^                ^
                                              dependen solo del producto_id

nombre_producto y precio_actual_producto dependen de producto_id solo, no de la combinación. Esa es DF parcial — viola 2FN.

Solución: mover esos atributos a la tabla producto:

-- Antes (mal):
CREATE TABLE pedido_linea (
    pedido_id INT,
    producto_id INT,
    cantidad INT,
    importe DECIMAL,
    nombre_producto VARCHAR(100),     -- ¡no!
    precio_producto DECIMAL,           -- ¡no!
    PRIMARY KEY (pedido_id, producto_id)
);

-- Después (bien):
CREATE TABLE pedido_linea (
    pedido_id INT,
    producto_id INT REFERENCES producto(id),
    cantidad INT,
    importe DECIMAL,
    PRIMARY KEY (pedido_id, producto_id)
);

CREATE TABLE producto (
    id SERIAL PRIMARY KEY,
    nombre VARCHAR(100),
    precio DECIMAL
);

Por qué importa:

  • Sin redundancia. El nombre del producto no se repite en cada línea.
  • Sin anomalías de actualización. Si cambia el nombre, una sola fila.
  • Más espacio. Líneas son chiquitas; el catálogo aparte.

Tablas con PK simple ya están automáticamente en 2FN (no hay "parte" de PK).

4.5 Tercera Forma Normal (3FN)

📐 Fundamento

Pre-requisito: la tabla está en 2FN.

Regla: ningún atributo no-clave depende transitivamente de la PK a través de otro no-clave.

Ejemplo de violación:

Tabla empleado:
PK = id

Atributos: id, nombre, departamento_id, departamento_nombre, departamento_telefono
                                        ^                   ^
                                        dependen de departamento_id, no de id directamente

DF transitiva: iddepartamento_iddepartamento_nombre\text{id} \rightarrow \text{departamento_id} \rightarrow \text{departamento_nombre}.

Solución: mover datos del departamento a su propia tabla:

-- Antes (mal):
CREATE TABLE empleado (
    id SERIAL PRIMARY KEY,
    nombre VARCHAR(100),
    departamento_id INT,
    departamento_nombre VARCHAR(100),       -- ¡no!
    departamento_telefono VARCHAR(20)        -- ¡no!
);

-- Después (bien):
CREATE TABLE departamento (
    id SERIAL PRIMARY KEY,
    nombre VARCHAR(100),
    telefono VARCHAR(20)
);

CREATE TABLE empleado (
    id SERIAL PRIMARY KEY,
    nombre VARCHAR(100),
    departamento_id INT REFERENCES departamento(id)
);

Resumen 3FN: cada atributo no-clave depende del PK, todo el PK, y nada más que el PK (frase clásica).

4.6 Boyce-Codd Normal Form (BCNF)

📐 Fundamento

Pre-requisito: la tabla está en 3FN.

Regla: para toda DF XYX \rightarrow Y no trivial, XX debe ser superclave (un superset de alguna llave candidata).

3FN deja un caso patológico que BCNF cierra: cuando una tabla tiene varias llaves candidatas que se solapan, puede haber DFs entre atributos clave que crean redundancia incluso en 3FN.

Ejemplo (concedido, raro):

Tabla curso_profesor_aula:
- (curso, profesor) → aula
- (aula, hora)     → curso

Posibles llaves candidatas: (curso, profesor) y (aula, hora).
Pero existe la DF: profesor → aula
                     ^         ^
                     no es llave   pero deriva

Ahí hay redundancia que 3FN no detecta, pero BCNF sí. La descomposición es más compleja.

En la práctica: la mayoría de las bases en buena 3FN están en BCNF. Casos donde difieren son raros. Si tu modelo entra en BCNF de natural, mejor; si no, 3FN es suficiente para 99% de los proyectos.

Más allá: existen 4FN (dependencias multivaluadas), 5FN, 6FN. Académicamente importantes, pero rara vez se aplican deliberadamente en la industria. 3FN o BCNF te llevan la mayor parte del camino.

4.7 Proceso de normalización

🛠️ En la práctica

Receta paso a paso:

  1. Listá los atributos del sistema, todos en una "tabla universal" (irreal).
  2. Identificá la PK.
  3. Listá las DFs que conocés del dominio.
  4. Aplicá 1FN: descomponé valores no-atómicos.
  5. Aplicá 2FN: remové DFs parciales.
  6. Aplicá 3FN: remové DFs transitivas.
  7. Verificá BCNF. Si falla, descomponé más.

Ejemplo paso a paso.

Tabla universal:

factura(numero, fecha, cliente_id, cliente_nombre, cliente_email,
        producto_codigo, producto_nombre, producto_precio,
        cantidad, subtotal_linea)
PK = (numero, producto_codigo)

DFs identificadas:

  • numerofecha,cliente_id\text{numero} \rightarrow \text{fecha}, \text{cliente_id}
  • cliente_idcliente_nombre,cliente_email\text{cliente_id} \rightarrow \text{cliente_nombre}, \text{cliente_email}
  • producto_codigoproducto_nombre,producto_precio\text{producto_codigo} \rightarrow \text{producto_nombre}, \text{producto_precio}
  • (numero,producto_codigo)cantidad,subtotal_linea(\text{numero}, \text{producto_codigo}) \rightarrow \text{cantidad}, \text{subtotal_linea}

1FN: OK, todos atómicos.

2FN: PK es compuesta. Atributos fecha, cliente_id, cliente_nombre, cliente_email dependen solo de numero. Atributos producto_nombre, producto_precio dependen solo de producto_codigo. Violación.

Descomposición 2FN:

factura(numero, fecha, cliente_id)              PK = numero
factura_linea(numero, producto_codigo, cantidad, subtotal_linea)   PK = (numero, producto_codigo)
producto(producto_codigo, nombre, precio)        PK = producto_codigo

Pero todavía tenemos cliente_nombre, cliente_email colgando de factura. ¿Dónde van? En cliente:

3FN: numerocliente_idcliente_nombre\text{numero} \rightarrow \text{cliente_id} \rightarrow \text{cliente_nombre} es transitiva. Violación.

Descomposición 3FN:

cliente(cliente_id, nombre, email)
factura(numero, fecha, cliente_id)
factura_linea(numero, producto_codigo, cantidad, subtotal)
producto(producto_codigo, nombre, precio)

Verificación BCNF: todas las DFs no triviales tienen PK como izquierda. ✅

Listo. De una tabla mal hecha pasamos a 4 tablas bien hechas.

4.8 Anomalías que evitamos

📐 Fundamento

Las formas normales eliminan tres tipos de anomalías:

Anomalía de inserción

No podés insertar un dato sin tener otro al cual no debería estar atado.

Ejemplo: en la tabla universal anterior, no podés insertar un cliente nuevo si no tiene factura. Tendrías que crear factura "vacía" — feo.

Solución: tabla cliente separada permite insertar clientes sin facturas.

Anomalía de actualización

Cambiar un dato requiere actualizar muchas filas. Si te olvidás alguna, queda inconsistente.

Ejemplo: cambiar cliente_nombre requeriría actualizar todas las filas de facturas de ese cliente.

Solución: una sola fila en cliente se actualiza una sola vez.

Anomalía de eliminación

Borrar un dato accidentalmente borra otra información valiosa.

Ejemplo: si borrás la única factura de un cliente, perdés sus datos personales.

Solución: tabla cliente separada conserva la info aunque se borren todas sus facturas.

4.9 Desnormalización deliberada

🛠️ En la práctica

¿Cuándo querer NO normalizar? A veces.

Razones para desnormalizar:

  1. Performance. JOINs son caros con tablas enormes. Cachear datos derivados puede acelerar lecturas.
  2. Reportes históricos. El precio del producto al momento del pedido debe quedar congelado en pedido_linea, no leído del catálogo (que cambió).
  3. Datawarehousing / OLAP. Las bases analíticas usan schema en estrella o copo de nieve que son intencionalmente desnormalizados para acelerar queries de agregación masivas.

Cuando es legítimo:

  • Hay medición concreta de problema de performance.
  • Hay disciplina para mantener consistencia (procedimientos, triggers, jobs).
  • El equipo entiende los trade-offs.

Ejemplo legítimo: guardar precio_unitario en pedido_linea:

CREATE TABLE pedido_linea (
    pedido_id INT,
    producto VARCHAR(10),
    cantidad INT,
    precio_unitario DECIMAL(10,2),       -- aparente desnormalización
    PRIMARY KEY (pedido_id, producto)
);

¿No es redundante respecto a producto.precio? No — porque es el precio al momento del pedido, no el precio actual. El precio del producto cambia mañana; el del pedido viejo no debería cambiar.

Cuando NO desnormalizar:

  • "Por si acaso es lento". Sin medirlo. Es la forma más común de mal-diseño.
  • Para "ahorrar JOINs". Los DBMS modernos los ejecutan rapidísimo si los índices están.

Regla: normalizá primero, desnormalizá si hace falta. Es más fácil agregar redundancia que quitarla.

4.10 Proyecto: pupusería normalizada

🏗️ Avance del proyecto

Verificamos que el esquema del cap 3 está en 3FN/BCNF, y discutimos las desnormalizaciones legítimas.

Tablas:

  • categoria(id, nombre, padre_id) — 3FN ✅
  • producto(codigo, nombre, descripcion, precio, stock, categoria_id) — 3FN ✅
  • cliente(id, nombre, email, telefono, fecha_registro) — 3FN ✅
  • empleado(id, nombre, rol, sueldo) — 3FN ✅
  • pedido(id, fecha, cliente_id, empleado_id, estado, total) — 3FN ✅ con observación
  • pedido_linea(pedido_id, producto, cantidad, importe) — 3FN ✅
  • proveedor(id, nombre, contacto) — 3FN ✅
  • compra(id, fecha, proveedor_id, total) — 3FN ✅
  • compra_linea(compra_id, producto, cantidad, costo_unitario) — 3FN ✅

Desnormalizaciones legítimas:

  1. pedido.total y compra.total son redundantes (se pueden calcular sumando líneas). Decisión: mantener, pero asegurarse vía:

    • Triggers que actualizan al modificar líneas.
    • O cálculo siempre antes de cerrar pedido.
  2. pedido_linea.importe es redundante (cantidad × precio_unitario). Decisión: mantener, porque guarda el precio histórico. Si el precio cambia mañana, el importe del pedido viejo sigue valiendo.

  3. No tenemos:

    • cliente.cantidad_pedidos — calcular cuando se necesite con COUNT.
    • producto.veces_vendido — idem.

    Esos serían desnormalizaciones agresivas que solo agregaríamos si midiéramos lentitud específica.

Observación importante. En sistemas reales se agregan también:

  • created_at, updated_at en cada tabla.
  • version para optimistic locking.
  • Soft delete con deleted_at.
  • Auditoría con tabla aparte.

Lo dejamos para cursos avanzados.

4.11 Resumen visual

Forma Regla
1FN Celdas atómicas, sin listas
2FN Sin DF parcial (solo aplica si PK compuesta)
3FN Sin DF transitiva
BCNF Toda DF tiene superclave a la izquierda
Anomalía Qué evita la normalización
Inserción No poder insertar X sin Y irrelevante
Actualización Cambiar un dato en múltiples filas
Eliminación Perder info al borrar otra cosa
Cuándo desnormalizar
Performance medido y crítico
Datos históricos (precio al momento del pedido)
Esquema OLAP / data warehouse
Nunca "por si acaso"

4.12 Ejercicios

✏️ Ejercicio 4.1 — Identificar la forma normal

¿En qué forma normal está esta tabla?

profesor(id, nombre, departamento_id, departamento_nombre, sueldo)
PK = id

DFs:

  • id → nombre, departamento_id, sueldo
  • departamento_id → departamento_nombre

✏️ Ejercicio 4.2 — Detectar anomalías

Esta tabla:

inscripcion(estudiante_id, materia_id, materia_nombre, nota, profesor)
PK = (estudiante_id, materia_id)

a. ¿En qué forma normal está? b. Identificá tres anomalías concretas.

✏️ Ejercicio 4.3 — Normalizar de cero

Tenés una hoja de cálculo de pedidos:

pedido fecha cliente tel_cliente producto cant precio total_pedido
1 1/5 María 55-12 queso 5 0.50 3.50
1 1/5 María 55-12 refresco 2 1.00 3.50
2 2/5 Juan 55-99 revuelta 4 0.60 2.40

Diseñá un esquema relacional normalizado a 3FN.

4.13 Cierre del libro

Cerramos Bases de Datos I. Tu recorrido:

  1. Modelo relacional — tablas, llaves, integridad.
  2. SQL básico — SELECT, JOIN, GROUP BY, subconsultas.
  3. Modelado ER — diseñar antes de codificar.
  4. Normalización — diseñar bien.

Listo para:

4.14 Para profundizar


Definiciones nuevas: dependencia funcional, DF parcial, DF transitiva, 1FN, 2FN, 3FN, BCNF, anomalías de inserción/actualización/eliminación, desnormalización, schema en estrella.