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:
- Redundancia. "María" y su email aparecen en cada línea de cada uno de sus pedidos.
- 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"?
- Anomalía de inserción. ¿Cómo agregás un cliente nuevo si todavía no compró nada? Necesitás un pedido vacío.
- 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: significa "X determina Y" — si conocés X, conocés Y de manera única.
Ejemplos:
- : una matrícula identifica únicamente al alumno.
- : el ISBN determina título y autor.
- : 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. . Cualquier atributo se determina a sí mismo. No aporta información.
- DF parcial. donde A es parte de la PK. Problema en 2FN.
- DF transitiva. , 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: .
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 no trivial, 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:
- Listá los atributos del sistema, todos en una "tabla universal" (irreal).
- Identificá la PK.
- Listá las DFs que conocés del dominio.
- Aplicá 1FN: descomponé valores no-atómicos.
- Aplicá 2FN: remové DFs parciales.
- Aplicá 3FN: remové DFs transitivas.
- 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:
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: 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:
- Performance. JOINs son caros con tablas enormes. Cachear datos derivados puede acelerar lecturas.
- 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ó). - 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ónpedido_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:
-
pedido.totalycompra.totalson 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.
-
pedido_linea.importees 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. -
No tenemos:
cliente.cantidad_pedidos— calcular cuando se necesite conCOUNT.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_aten cada tabla.versionpara 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
Solución
- 1FN ✅ (celdas atómicas)
- 2FN ✅ (PK simple, no hay DF parcial)
- 3FN ❌ —
id → departamento_id → departamento_nombrees transitiva.
Está en 2FN, no en 3FN.
Solución:
profesor(id, nombre, departamento_id, sueldo) -- PK = id
departamento(id, nombre) -- PK = id
✏️ 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.
Solución
a. 1FN sí. 2FN no — materia_nombre y profesor dependen solo de materia_id, no de la PK compuesta. Está en 1FN.
b.
- Anomalía de inserción. No podés crear una materia sin que un estudiante esté inscripto.
- Anomalía de actualización. Si la materia cambia de profesor, hay que actualizar todas las filas de inscripción.
- Anomalía de eliminación. Si todos los estudiantes se desinscriben, perdés la info de la materia.
Solución (descomposición a 3FN):
materia(id, nombre, profesor)
inscripcion(estudiante_id, materia_id, nota)
✏️ 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.
Solución
CREATE TABLE cliente (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
telefono VARCHAR(20)
);
CREATE TABLE producto (
nombre VARCHAR(50) PRIMARY KEY, -- o un id artificial
precio DECIMAL(10,2) NOT NULL
);
CREATE TABLE pedido (
id SERIAL PRIMARY KEY,
fecha DATE NOT NULL,
cliente_id INTEGER REFERENCES cliente(id),
total DECIMAL(10,2) -- desnormalización aceptable
);
CREATE TABLE pedido_linea (
pedido_id INTEGER REFERENCES pedido(id) ON DELETE CASCADE,
producto VARCHAR(50) REFERENCES producto(nombre),
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
PRIMARY KEY (pedido_id, producto)
);
Datos transferidos quedan limpios:
INSERT INTO cliente (nombre, telefono) VALUES
('María', '55-12'),
('Juan', '55-99');
INSERT INTO producto VALUES
('queso', 0.50),
('refresco', 1.00),
('revuelta', 0.60);
INSERT INTO pedido (fecha, cliente_id, total) VALUES
('2026-05-01', 1, 3.50),
('2026-05-02', 2, 2.40);
INSERT INTO pedido_linea VALUES
(1, 'queso', 5),
(1, 'refresco', 2),
(2, 'revuelta', 4);
4.13 Cierre del libro
Cerramos Bases de Datos I. Tu recorrido:
- Modelo relacional — tablas, llaves, integridad.
- SQL básico — SELECT, JOIN, GROUP BY, subconsultas.
- Modelado ER — diseñar antes de codificar.
- Normalización — diseñar bien.
Listo para:
- Bases de Datos II (BDD415): transacciones, índices avanzados, performance, replicación, NoSQL.
- Análisis y Diseño de Sistemas (ADS415): UML, casos de uso, diseño full-stack.
- Aplicar SQL en proyectos reales. Web app + Postgres = combo de la industria.
4.14 Para profundizar
- Date, Database in Depth. Riguroso, denso, espectacular.
- Garcia-Molina, Ullman, Widom, Database Systems: The Complete Book. Universitario clásico.
- Documentación de PostgreSQL sobre constraints y schema design.
- PluralSight / Coursera — cursos completos sobre database design.
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.