En esta página
Normalización de bases de datos
Normalización de bases de datos
La normalización es el proceso de diseñar un esquema de base de datos para reducir la redundancia de datos y prevenir anomalías de actualización, inserción y eliminación. Es la diferencia entre un esquema que crece limpiamente con el tiempo y uno que se convierte en una pesadilla de inconsistencias.
¿Por qué importa la normalización?
Considera esta tabla "todo en uno" que podrías diseñar si no conoces normalización:
-- ❌ Tabla mal diseñada: redundancias y anomalías
CREATE TABLE pedidos_mal (
pedido_id INTEGER,
cliente_nombre VARCHAR(150), -- Repetido en cada pedido del mismo cliente
cliente_email VARCHAR(255), -- ¿Qué pasa si cambia el email?
cliente_ciudad VARCHAR(100), -- Redundante en cada fila
producto_nombre VARCHAR(200), -- Un pedido con 5 productos = 5 filas
producto_precio NUMERIC(10,2), -- ¿Cuál es el precio actual del producto?
categoria_nombre VARCHAR(100), -- Repetida para cada producto de esa categoría
cantidad INTEGER,
total_pedido NUMERIC(10,2) -- Calculado: ¿y si alguien lo actualiza mal?
);
-- Ejemplo de datos con problemas:
INSERT INTO pedidos_mal VALUES
(1, 'Ana García', '[email protected]', 'Madrid', 'Laptop Pro', 1299.99, 'Electrónica', 1, 1299.99),
(1, 'Ana García', '[email protected]', 'Madrid', 'Mouse Pro', 49.99, 'Electrónica', 2, 199.98),
(2, 'Ana García', '[email protected]', 'Madrid', 'Libro SQL', 35.99, 'Libros', 1, 35.99);
-- ¡El email de Ana aparece diferente en el pedido 2! Inconsistencia.Los problemas de este diseño:
- Anomalía de actualización: si Ana cambia su email, hay que actualizar todas sus filas (¿cuántas son?).
- Anomalía de inserción: no puedes registrar un nuevo cliente sin que haga al menos un pedido.
- Anomalía de eliminación: si se borra el único pedido de un cliente, también se pierde su información.
- Redundancia: el nombre de la categoría se repite en cada producto, y en cada pedido de ese producto.
Primera Forma Normal (1NF)
Una tabla está en 1NF si:
- Cada columna contiene valores atómicos (indivisibles, no listas ni conjuntos).
- No hay grupos repetitivos de columnas.
- Cada fila es única (tiene una clave primaria).
-- ❌ Viola 1NF: columna con múltiples valores
CREATE TABLE clientes_mal (
id SERIAL PRIMARY KEY,
nombre VARCHAR(150),
telefonos TEXT -- '612345678, 698765432, 634567890' — ¡NO atómico!
);
-- ✅ Correcto: tabla separada para teléfonos
CREATE TABLE clientes (
id SERIAL PRIMARY KEY,
nombre VARCHAR(150) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE
);
CREATE TABLE clientes_telefonos (
id SERIAL PRIMARY KEY,
cliente_id INTEGER NOT NULL REFERENCES clientes(id) ON DELETE CASCADE,
telefono VARCHAR(20) NOT NULL,
tipo VARCHAR(20) DEFAULT 'movil' -- 'movil', 'fijo', 'trabajo'
);
-- ❌ También viola 1NF: grupos repetitivos de columnas
CREATE TABLE pedidos_mal2 (
pedido_id INTEGER PRIMARY KEY,
producto1_id INTEGER,
producto1_qty INTEGER,
producto2_id INTEGER,
producto2_qty INTEGER,
producto3_id INTEGER,
producto3_qty INTEGER
-- ¿Y si el pedido tiene 10 productos?
);
-- ✅ Correcto: tabla de líneas de pedido separada
CREATE TABLE pedidos_lineas (
pedido_id INTEGER NOT NULL REFERENCES pedidos(id),
producto_id INTEGER NOT NULL REFERENCES productos(id),
cantidad INTEGER NOT NULL DEFAULT 1,
precio_unit NUMERIC(10,2) NOT NULL,
PRIMARY KEY (pedido_id, producto_id)
);Segunda Forma Normal (2NF)
Una tabla está en 2NF si:
- Está en 1NF.
- Todos los atributos no-clave dependen de la clave primaria completa (no solo de parte de ella).
La 2NF solo es relevante cuando la clave primaria es compuesta (múltiples columnas):
-- ❌ Viola 2NF: la tabla tiene PK compuesta (pedido_id, producto_id)
-- pero 'nombre_producto' depende SOLO de producto_id, no de la PK completa
CREATE TABLE pedidos_detalle_mal (
pedido_id INTEGER,
producto_id INTEGER,
nombre_producto VARCHAR(200), -- Depende solo de producto_id (dependencia parcial)
categoria VARCHAR(100), -- Depende solo de producto_id
cantidad INTEGER,
precio_unit NUMERIC(10,2),
PRIMARY KEY (pedido_id, producto_id)
);
-- ✅ Correcto en 2NF: separar la información del producto
-- Los atributos del producto viven en la tabla 'productos'
-- La tabla intermedia solo tiene atributos que dependen de la PK completa
CREATE TABLE pedidos_items (
pedido_id INTEGER NOT NULL REFERENCES pedidos(id),
producto_id INTEGER NOT NULL REFERENCES productos(id),
cantidad INTEGER NOT NULL,
precio_unit NUMERIC(10,2) NOT NULL, -- El precio al momento de la compra
PRIMARY KEY (pedido_id, producto_id)
);
-- 'cantidad' y 'precio_unit' dependen del par (pedido, producto) completo
-- Los datos del producto (nombre, categoría) se consultan con JOINTercera Forma Normal (3NF)
Una tabla está en 3NF si:
- Está en 2NF.
- No hay dependencias transitivas: ningún atributo no-clave depende de otro atributo no-clave.
-- ❌ Viola 3NF: dependencia transitiva
-- ciudad_nombre depende de ciudad_codigo, que NO es la clave primaria
CREATE TABLE empleados_mal (
id SERIAL PRIMARY KEY,
nombre VARCHAR(150),
ciudad_codigo VARCHAR(10), -- No es PK
ciudad_nombre VARCHAR(100), -- Depende de ciudad_codigo (transitiva)
pais VARCHAR(50) -- Depende de ciudad_codigo (transitiva)
);
-- ✅ Correcto en 3NF: extraer la entidad 'ciudad'
CREATE TABLE ciudades (
codigo VARCHAR(10) PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
pais VARCHAR(50) NOT NULL
);
CREATE TABLE empleados (
id SERIAL PRIMARY KEY,
nombre VARCHAR(150) NOT NULL,
ciudad_codigo VARCHAR(10) REFERENCES ciudades(codigo)
);
-- ✅ Ejemplo más realista con empleados y departamentos
CREATE TABLE departamentos (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
presupuesto NUMERIC(12,2),
director_id INTEGER -- Se llena después
);
CREATE TABLE empleados_normalizado (
id SERIAL PRIMARY KEY,
nombre VARCHAR(150) NOT NULL,
salario NUMERIC(10,2) NOT NULL,
departamento_id INTEGER REFERENCES departamentos(id)
-- el nombre y presupuesto del departamento están en la tabla departamentos
-- no aquí (evita la dependencia transitiva)
);Forma Normal de Boyce-Codd (BCNF)
BCNF es una versión más estricta de 3NF. Una tabla está en BCNF si para cada dependencia funcional X → Y, X es una superclave (puede determinar unívocamente cualquier fila).
En la práctica, la diferencia entre 3NF y BCNF solo aparece en casos con múltiples claves candidatas superpuestas, lo que es poco común. Para la mayoría de diseños empresariales, llegar a 3NF es suficiente.
Aplicar la normalización: caso práctico completo
Partimos de un Excel típico que nos da un cliente y lo llevamos a 3NF:
-- Excel original del cliente (todo en una tabla)
-- pedido | fecha | cliente | email | ciudad | producto | cat | precio | qty
-- 1001 | 2024-01-15 | Ana García | [email protected] | Madrid | Laptop Pro | Electro | 1299 | 1
-- 1001 | 2024-01-15 | Ana García | [email protected] | Madrid | Mouse Inalám | Electro | 49.99 | 2
-- 1002 | 2024-01-20 | Luis Mart | [email protected] | Barcel | Clean Code | Libros | 35.99 | 1
-- ✅ Diseño normalizado en 3NF:
CREATE TABLE categorias (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL UNIQUE
);
CREATE TABLE productos (
id SERIAL PRIMARY KEY,
nombre VARCHAR(200) NOT NULL,
precio_base NUMERIC(10,2) NOT NULL CHECK (precio_base >= 0),
categoria_id INTEGER NOT NULL REFERENCES categorias(id)
);
CREATE TABLE clientes (
id SERIAL PRIMARY KEY,
nombre VARCHAR(150) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE, -- Email es clave candidata
ciudad VARCHAR(100)
);
CREATE TABLE pedidos (
id SERIAL PRIMARY KEY,
cliente_id INTEGER NOT NULL REFERENCES clientes(id),
fecha TIMESTAMPTZ NOT NULL DEFAULT NOW(),
estado VARCHAR(20) NOT NULL DEFAULT 'pendiente'
);
CREATE TABLE pedidos_lineas (
pedido_id INTEGER NOT NULL REFERENCES pedidos(id) ON DELETE CASCADE,
producto_id INTEGER NOT NULL REFERENCES productos(id),
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
precio_unit NUMERIC(10,2) NOT NULL, -- Precio histórico (puede cambiar)
PRIMARY KEY (pedido_id, producto_id)
);
-- Cargar los datos del Excel original
INSERT INTO categorias (nombre) VALUES ('Electrónica'), ('Libros');
INSERT INTO productos (nombre, precio_base, categoria_id) VALUES
('Laptop Pro', 1299.00, 1),
('Mouse Inalámbrico', 49.99, 1),
('Clean Code', 35.99, 2);
INSERT INTO clientes (nombre, email, ciudad) VALUES
('Ana García', '[email protected]', 'Madrid'),
('Luis Martínez', '[email protected]', 'Barcelona');
INSERT INTO pedidos (cliente_id, fecha) VALUES (1, '2024-01-15'), (2, '2024-01-20');
INSERT INTO pedidos_lineas (pedido_id, producto_id, cantidad, precio_unit) VALUES
(1, 1, 1, 1299.00), (1, 2, 2, 49.99), (2, 3, 1, 35.99);Desnormalización estratégica
La normalización es el punto de partida, pero a veces desnormalizar intencionalmente mejora el rendimiento de consultas críticas. La clave es hacerlo conscientemente, con una razón medible:
-- Caso: tabla de eventos de analytics con millones de filas por día
-- Hacer JOIN con clientes para obtener su ciudad en cada consulta es costoso
-- ❌ Normalizado (lento para analytics)
SELECT
e.tipo_evento,
c.ciudad,
COUNT(*) AS eventos
FROM eventos e
JOIN clientes c ON e.cliente_id = c.id
WHERE e.fecha >= '2024-01-01'
GROUP BY e.tipo_evento, c.ciudad;
-- ✅ Desnormalizado (ciudad copiada en la tabla de eventos)
CREATE TABLE eventos (
id BIGSERIAL PRIMARY KEY,
cliente_id INTEGER NOT NULL,
ciudad VARCHAR(100), -- Desnormalización: copia de clientes.ciudad
tipo_evento VARCHAR(50) NOT NULL,
fecha TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Ahora la consulta analítica no necesita JOIN:
SELECT tipo_evento, ciudad, COUNT(*) AS eventos
FROM eventos
WHERE fecha >= '2024-01-01'
GROUP BY tipo_evento, ciudad;
-- ¿Y si cambia la ciudad del cliente?
-- En analytics histórico, generalmente NO actualizas los eventos pasados:
-- los eventos ocurrieron en el momento en que el cliente vivía en esa ciudad.Resumen: guía de decisión
| Situación | Decisión |
|---|---|
| Datos maestros (clientes, productos, categorías) | Normalizar a 3NF |
| Datos transaccionales (pedidos, pagos) | Normalizar a 3NF, guardar precios históricos |
| Reportes OLAP de alto volumen | Considerar desnormalización o vistas materializadas |
| Datos de auditoría/logs | Desnormalizar (los datos históricos no cambian) |
| Datos con estructura variable | JSONB puede ser apropiado |
[!tip] Mide antes de desnormalizar: no desnormalices "por si acaso". Primero diseña en 3NF, luego mide el rendimiento real con datos reales. Si una consulta específica es demasiado lenta y los índices no son suficientes, evalúa la desnormalización solo para ese caso. Las vistas materializadas son frecuentemente una alternativa mejor: te dan el rendimiento de la desnormalización sin sacrificar la integridad del modelo normalizado.
La normalización es el fundamento del buen diseño de bases de datos. En la siguiente lección profundizaremos en los mecanismos de integridad: claves primarias, foráneas, restricciones CHECK, valores DEFAULT y cómo PostgreSQL garantiza que tus datos siempre sean válidos.
nextSteps:
- slug: claves-y-restricciones label: "Siguiente: Claves y restricciones"
Inicia sesión para guardar tu progreso