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ónAl actualizar/eliminar el padre
NO ACTIONError si existen filas hijas (predeterminado)
CASCADEActualiza/elimina las filas hijas también
SET NULLPone NULL en la columna FK del hijo
SET DEFAULTPone 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);