Volver a SQL Básico
Diseño de Esquema y Normalización
Un buen diseño de esquema previene anomalías de datos y hace las consultas eficientes.
Normalización
La normalización elimina la redundancia organizando los datos en tablas bien estructuradas.
Primera Forma Normal (1FN)
Cada columna debe contener valores atómicos (indivisibles):
-- ❌ NO ES 1FN — múltiples valores en una columna
CREATE TABLE pedidos_mal (
id INT,
cliente VARCHAR(100),
productos VARCHAR(500) -- "Laptop, Teléfono, Auriculares"
);
-- ✅ 1FN — valores atómicos, filas separadas
CREATE TABLE items_pedido (
id INT PRIMARY KEY,
pedido_id INT,
producto_id INT,
cantidad INT
);
Segunda Forma Normal (2FN)
Todas las columnas no clave deben depender de la clave primaria completa:
-- ❌ NO ES 2FN — nombre_producto depende solo de producto_id
CREATE TABLE items_pedido_mal (
pedido_id INT,
producto_id INT,
nombre_producto VARCHAR(100), -- ¡depende solo de producto_id!
cantidad INT,
PRIMARY KEY (pedido_id, producto_id)
);
-- ✅ 2FN — tabla de productos separada
CREATE TABLE productos (
id INT PRIMARY KEY,
nombre VARCHAR(100)
);
CREATE TABLE items_pedido (
pedido_id INT,
producto_id INT REFERENCES productos(id),
cantidad INT,
PRIMARY KEY (pedido_id, producto_id)
);
Tercera Forma Normal (3FN)
Sin dependencias transitivas:
-- ❌ NO ES 3FN — codigo_postal → ciudad (transitiva)
CREATE TABLE clientes_mal (
id INT PRIMARY KEY,
nombre VARCHAR(100),
codigo_postal VARCHAR(10),
ciudad VARCHAR(100) -- depende de codigo_postal, no de id
);
-- ✅ 3FN
CREATE TABLE codigos_postales (
codigo_postal VARCHAR(10) PRIMARY KEY,
ciudad VARCHAR(100)
);
CREATE TABLE clientes (
id INT PRIMARY KEY,
nombre VARCHAR(100),
codigo_postal VARCHAR(10) REFERENCES codigos_postales(codigo_postal)
);
Restricciones
CREATE TABLE productos (
id INT PRIMARY KEY AUTO_INCREMENT,
sku VARCHAR(50) NOT NULL UNIQUE,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(10,2) NOT NULL,
stock INT NOT NULL DEFAULT 0,
estado VARCHAR(20) NOT NULL DEFAULT 'activo',
categoria_id INT,
-- Restricciones de verificación
CONSTRAINT chk_precio CHECK (precio > 0),
CONSTRAINT chk_stock CHECK (stock >= 0),
CONSTRAINT chk_estado CHECK (estado IN ('activo', 'inactivo', 'descontinuado')),
-- Clave foránea con comportamiento en cascada
CONSTRAINT fk_categoria
FOREIGN KEY (categoria_id)
REFERENCES categorias(id)
ON UPDATE CASCADE
ON DELETE SET NULL
);
Opciones de Cascada en Claves Foráneas
| Opción | Al actualizar/eliminar el padre |
|---|---|
NO ACTION | Error si existen filas hijas (predeterminado) |
CASCADE | Actualiza/elimina las filas hijas también |
SET NULL | Pone NULL en la columna FK del hijo |
SET DEFAULT | Pone el valor por defecto en el hijo |
Desnormalización (Cuándo Romper las Reglas)
-- ✅ Tabla de resumen desnormalizada para dashboards
CREATE TABLE resumen_pedidos (
pedido_id INT PRIMARY KEY,
nombre_cliente VARCHAR(100), -- redundante pero evita un JOIN
cantidad_items INT,
total DECIMAL(10,2),
creado_en TIMESTAMP
);
Vistas
Las vistas son tablas virtuales definidas por una consulta:
-- Crear una vista
CREATE VIEW resumen_clientes AS
SELECT
c.id,
c.nombre,
c.apellido,
COUNT(p.id) AS cantidad_pedidos,
COALESCE(SUM(p.total), 0) AS valor_vida
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id, c.nombre, c.apellido;
-- Usarla como una tabla
SELECT * FROM resumen_clientes WHERE valor_vida > 500;
Procedimientos Almacenados
DELIMITER $$
CREATE PROCEDURE ObtenerMejoresClientes(
IN gasto_minimo DECIMAL(10,2),
IN limite_resultados INT
)
BEGIN
SELECT c.id, CONCAT(c.nombre, ' ', c.apellido) AS nombre, SUM(p.total) AS gastado
FROM clientes c
JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id
HAVING SUM(p.total) >= gasto_minimo
ORDER BY gastado DESC
LIMIT limite_resultados;
END$$
DELIMITER ;
-- Llamarlo
CALL ObtenerMejoresClientes(500.00, 10);