El diseño de bases de datos es un proceso sistemático para crear un modelo eficiente que almacene y gestione datos sin redundancias, garantizando integridad y rendimiento. Es fundamental en el desarrollo de aplicaciones modernas.
Basado en tablas con relaciones definidas por claves primarias y foráneas. Ejemplos: MySQL, PostgreSQL.
Estructuras flexibles como documentos (MongoDB), grafos (Neo4j) o clave-valor (Redis). Ideal para datos no estructurados.
// Esquema simplificado para un sistema de ventas
CREATE TABLE Clientes (
id INT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL
);
CREATE TABLE Pedidos (
id INT PRIMARY KEY,
cliente_id INT,
fecha DATE,
FOREIGN KEY (cliente_id) REFERENCES Clientes(id)
);
Este diseño básico ilustra una relación 1 a N (un cliente puede tener múltiples pedidos).
El diseño de bases de datos es un proceso fundamental en el desarrollo de sistemas de información. Según estudios, el 70% de los problemas de rendimiento en aplicaciones se originan en un diseño deficiente de la base de datos.
Objetos del mundo real (Ej: Cliente, Producto)
Características de las entidades (Ej: nombre, precio)
Conexiones entre entidades (1:1, 1:N, N:M)
Diagrama que muestra las entidades con relaciones
Diagrama que muestra las entidades con relaciones
Característica | Descripción | Beneficio |
---|---|---|
Normalización | Estructuración en formas normales (1FN, 2FN, 3FN) | Elimina redundancias y anomalías |
Índices | Estructuras para acelerar búsquedas frecuentes | Mejora rendimiento en consultas |
Integridad | Reglas como claves primarias/foráneas | Garantiza consistencia de datos |
Necesitamos almacenar información sobre:
Modelo con entidades Paciente, Doctor, Cita y Medicamento
-- Tabla Pacientes
CREATE TABLE Pacientes (
id_paciente INT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
fecha_nacimiento DATE,
tipo_sangre VARCHAR(3)
);
-- Tabla Doctores
CREATE TABLE Doctores (
id_doctor INT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
especialidad VARCHAR(50),
telefono VARCHAR(15)
);
-- Tabla Citas
CREATE TABLE Citas (
id_cita INT PRIMARY KEY,
id_paciente INT,
id_doctor INT,
fecha_hora DATETIME,
motivo VARCHAR(200),
FOREIGN KEY (id_paciente) REFERENCES Pacientes(id_paciente),
FOREIGN KEY (id_doctor) REFERENCES Doctores(id_doctor)
);
.......
.......
.......
Dividir campos compuestos (ej: dirección en calle, ciudad, CP)
Eliminar atributos que dependen parcialmente de claves compuestas
Remover atributos que dependen de otros no clave (ej: edad → fecha_nacimiento)
Estructura para análisis con tablas de hechos y dimensiones
Diferencias en estructura y escalabilidad
Modelo con entidades Paciente, Doctor, Cita y Medicamento
La normalización es un proceso sistemático para organizar datos en una base de datos relacional, con dos objetivos principales:
📜 Historia: Desarrollada por Edgar F. Codd en los 70s como parte del modelo relacional. Las primeras tres formas normales (1FN, 2FN, 3FN) son las más utilizadas en aplicaciones prácticas.
Cuando el valor de un atributo determina otro (ej: ID → Nombre)
Atributo(s) que identifican registros únicamente
Clave candidata seleccionada como identificador principal
Regla: Todos los atributos son atómicos (indivisibles)
Ejemplo:
ID | Nombre | Teléfonos (NO 1FN) |
---|---|---|
1 | Ana | 555-1234, 555-5678 |
➔ Dividir en múltiples registros
Regla: Cumple 1FN + atributos dependen de toda clave primaria
Ejemplo problema:
ID_Orden | ID_Producto | Cantidad | Nombre_Producto |
---|---|---|---|
100 | P001 | 2 | Laptop |
➔ Separar en tablas Ordenes y Productos
Regla: Cumple 2FN + no hay dependencias transitivas
Ejemplo problema:
ID_Empleado | Departamento | Jefe_Departamento |
---|---|---|
E01 | Ventas | Juan Pérez |
➔ Crear tabla Departamentos separada
CREATE TABLE Pedidos (
id_pedido INT,
fecha DATE,
id_cliente INT,
nombre_cliente VARCHAR(100),
direccion_cliente VARCHAR(200),
id_producto INT,
nombre_producto VARCHAR(100),
categoria_producto VARCHAR(50),
precio DECIMAL(10,2),
cantidad INT
);
🔴 Problemas detectados: Redundancia de datos, atributos no atómicos, dependencias parciales.
-- Separar direcciones compuestas
CREATE TABLE Pedidos_1FN (
id_pedido INT,
fecha DATE,
id_cliente INT,
nombre_cliente VARCHAR(100),
calle_cliente VARCHAR(100),
ciudad_cliente VARCHAR(50),
cp_cliente VARCHAR(10),
-- ... otros campos
);
-- Crear tablas separadas
CREATE TABLE Clientes (
id_cliente INT PRIMARY KEY,
nombre VARCHAR(100),
calle VARCHAR(100),
ciudad VARCHAR(50),
cp VARCHAR(10)
);
CREATE TABLE Productos (
id_producto INT PRIMARY KEY,
nombre VARCHAR(100),
categoria VARCHAR(50),
precio DECIMAL(10,2)
);
CREATE TABLE Pedidos_2FN (
id_pedido INT PRIMARY KEY,
fecha DATE,
id_cliente INT,
FOREIGN KEY (id_cliente) REFERENCES Clientes(id_cliente)
);
-- Eliminar dependencia transitiva categoría→precio
CREATE TABLE Categorias (
id_categoria INT PRIMARY KEY,
nombre VARCHAR(50),
margen_ganancia DECIMAL(5,2)
);
ALTER TABLE Productos
ADD COLUMN id_categoria INT,
ADD FOREIGN KEY (id_categoria) REFERENCES Categorias(id_categoria);
Forma Normal | Regla | Solución Típica |
---|---|---|
1FN | Atributos atómicos | Dividir campos compuestos |
2FN | Dependencia completa | Separar en tablas relacionadas |
3FN | No dependencias transitivas | Extraer entidades independientes |
Versión reforzada de 3FN que maneja casos especiales de dependencias funcionales.
Ejemplo: Cuando un atributo no clave determina parte de una clave candidata.
Tratan dependencias multivaluadas y de reunión, usadas en diseños muy especializados.
Aplicación: Sistemas complejos con relaciones muchos-a-muchos multidimensionales.
ID | Nombre | Teléfonos | Cursos |
---|---|---|---|
E101 | Ana García | 555-1234, 555-5678 | Matemáticas:8.5, Física:7.9 |
ID | Nombre |
---|---|
E101 | Ana García |
ID_Estudiante | Teléfono |
---|---|
E101 | 555-1234 |
E101 | 555-5678 |
ID_Estudiante | Curso | Nota |
---|---|---|
E101 | Matemáticas | 8.5 |
E101 | Física | 7.9 |
-- Estructura 1FN
CREATE TABLE Estudiantes (
ID_Estudiante VARCHAR(10) PRIMARY KEY,
Nombre VARCHAR(100)
);
CREATE TABLE Telefonos (
ID_Estudiante VARCHAR(10),
Telefono VARCHAR(15),
PRIMARY KEY (ID_Estudiante, Telefono),
FOREIGN KEY (ID_Estudiante) REFERENCES Estudiantes(ID_Estudiante)
);
CREATE TABLE Calificaciones (
ID_Estudiante VARCHAR(10),
Curso VARCHAR(50),
Nota DECIMAL(3,1),
PRIMARY KEY (ID_Estudiante, Curso),
FOREIGN KEY (ID_Estudiante) REFERENCES Estudiantes(ID_Estudiante)
);
ID_Pedido | Fecha | ID_Producto | Nombre_Producto | Categoría | Cantidad |
---|---|---|---|---|---|
P1001 | 2023-05-10 | PRD001 | Laptop Elite | Electrónica | 2 |
ID_Pedido | Fecha |
---|---|
P1001 | 2023-05-10 |
ID_Producto | Nombre | Categoría |
---|---|---|
PRD001 | Laptop Elite | Electrónica |
ID_Pedido | ID_Producto | Cantidad |
---|---|---|
P1001 | PRD001 | 2 |
-- Estructura 2FN
CREATE TABLE Pedidos (
ID_Pedido VARCHAR(10) PRIMARY KEY,
Fecha DATE
);
CREATE TABLE Productos (
ID_Producto VARCHAR(10) PRIMARY KEY,
Nombre VARCHAR(100),
Categoría VARCHAR(50)
);
CREATE TABLE Detalle_Pedidos (
ID_Pedido VARCHAR(10),
ID_Producto VARCHAR(10),
Cantidad INT,
PRIMARY KEY (ID_Pedido, ID_Producto),
FOREIGN KEY (ID_Pedido) REFERENCES Pedidos(ID_Pedido),
FOREIGN KEY (ID_Producto) REFERENCES Productos(ID_Producto)
);
ID_Empleado | Nombre | Departamento | Jefe_Departamento | Teléfono_Jefe |
---|---|---|---|---|
EMP101 | Carlos Ruiz | Ventas | María López | 555-9876 |
ID_Empleado | Nombre | ID_Departamento |
---|---|---|
EMP101 | Carlos Ruiz | DEPT01 |
ID_Departamento | Nombre | ID_Jefe |
---|---|---|
DEPT01 | Ventas | EMP201 |
ID_Jefe | Nombre | Teléfono |
---|---|---|
EMP201 | María López | 555-9876 |
-- Estructura 3FN
CREATE TABLE Jefes (
ID_Jefe VARCHAR(10) PRIMARY KEY,
Nombre VARCHAR(100),
Teléfono VARCHAR(15)
);
CREATE TABLE Departamentos (
ID_Departamento VARCHAR(10) PRIMARY KEY,
Nombre VARCHAR(50),
ID_Jefe VARCHAR(10),
FOREIGN KEY (ID_Jefe) REFERENCES Jefes(ID_Jefe)
);
CREATE TABLE Empleados (
ID_Empleado VARCHAR(10) PRIMARY KEY,
Nombre VARCHAR(100),
ID_Departamento VARCHAR(10),
FOREIGN KEY (ID_Departamento) REFERENCES Departamentos(ID_Departamento)
);
Forma Normal | Antes | Después | Beneficio Clave |
---|---|---|---|
1FN | Campos compuestos/multivalor | Datos atómicos en tablas separadas | Elimina redundancia horizontal |
2FN | Dependencias parciales | Tablas especializadas | Elimina redundancia vertical |
3FN | Dependencias transitivas | Jerarquía de entidades | Mayor consistencia |
Técnica que introduce redundancia controlada en un diseño de base de datos previamente normalizado, con el objetivo de:
Mejorar el rendimiento de consultas
Optimizar sistemas de reporting
Simplificar consultas complejas
Consultas frecuentes que necesitan el nombre del cliente junto con los pedidos
SELECT p.id, p.fecha, c.nombre
FROM pedidos p
JOIN clientes c ON p.cliente_id = c.id;
ALTER TABLE pedidos
ADD COLUMN cliente_nombre VARCHAR(100);
-- Actualizado con trigger
CREATE TRIGGER actualizar_nombre_cliente
AFTER INSERT ON pedidos
FOR EACH ROW
UPDATE pedidos
SET cliente_nombre = (SELECT nombre FROM clientes WHERE id = NEW.cliente_id)
WHERE id = NEW.id;
Ventaja: Elimina JOIN en consultas frecuentes
Costo: Requiere actualización cuando cambia el nombre
Reportes que calculan totales de ventas por producto
SELECT p.id, p.nombre, SUM(dp.cantidad) as total_vendido
FROM productos p
JOIN detalles_pedido dp ON p.id = dp.producto_id
GROUP BY p.id, p.nombre;
CREATE TABLE resumen_ventas (
producto_id INT PRIMARY KEY,
nombre_producto VARCHAR(100),
total_vendido INT,
ultima_actualizacion TIMESTAMP
);
-- Actualizado por job nocturno o triggers
Ventaja: Consultas instantáneas de métricas
Costo: Datos pueden estar ligeramente desactualizados
Consultas que acceden frecuentemente a datos de tablas relacionadas
SELECT a.titulo, u.nombre, c.nombre as categoria
FROM articulos a
JOIN usuarios u ON a.autor_id = u.id
JOIN categorias c ON a.categoria_id = c.id;
ALTER TABLE articulos
ADD COLUMN autor_nombre VARCHAR(100),
ADD COLUMN categoria_nombre VARCHAR(50);
-- Actualizado con triggers o aplicación
Ventaja: Elimina múltiples JOINs
Costo: Mayor espacio y complejidad en actualizaciones
La página de producto requiere datos de 5 tablas diferentes, causando alta latencia.
CREATE TABLE producto_detalle (
producto_id INT PRIMARY KEY,
nombre VARCHAR(200),
descripcion TEXT,
precio DECIMAL(10,2),
categoria_nombre VARCHAR(50),
promedio_valoracion DECIMAL(3,2),
inventario INT,
atributos JSON, -- {color: "rojo", talla: "XL"}
ultima_actualizacion TIMESTAMP
);
El feed requiere unir datos de usuarios, publicaciones, likes y comentarios.
CREATE TABLE feed_usuario (
usuario_id INT,
publicacion_id INT,
contenido TEXT,
autor_id INT,
autor_nombre VARCHAR(100),
autor_foto VARCHAR(255),
likes INT,
comentarios INT,
fecha TIMESTAMP,
PRIMARY KEY (usuario_id, publicacion_id)
);
Reportes ejecutivos tardan minutos en generarse por agregaciones complejas.
CREATE TABLE snapshot_metricas (
fecha DATE PRIMARY KEY,
ventas_totales DECIMAL(12,2),
nuevos_usuarios INT,
pedidos_completados INT,
productos_populares JSON,
metricas_region JSON,
actualizado_a TIMESTAMP
);
Problema: La página de artículo requiere unir todas estas tablas, causando alta latencia (~1.2s).
CREATE TABLE vista_articulo (
articulo_id INT PRIMARY KEY,
titulo VARCHAR(200),
contenido TEXT,
autor_id INT,
autor_nombre VARCHAR(100),
categoria_id INT,
categoria_nombre VARCHAR(50),
total_comentarios INT,
total_likes INT,
ultimos_comentarios JSON, -- [{usuario: "Ana", texto: "..."}]
fecha_actualizacion TIMESTAMP
);
-- Mecanismos de actualización:
-- 1. Trigger al publicar/editar artículo
-- 2. Job periódico para contadores
-- 3. Eventos en tiempo real para comentarios recientes
La consulta del catálogo público requiere 4 JOINs, tardando 1.8 segundos con 10k libros.
SELECT
l.titulo,
a.nombre AS autor,
e.nombre AS editorial,
COUNT(ej.id) AS copias_disponibles
FROM libros l
JOIN autores a ON l.autor_id = a.id
JOIN editoriales e ON l.editorial_id = e.id
LEFT JOIN ejemplares ej ON l.id = ej.libro_id
WHERE ej.disponible = true
GROUP BY l.id, a.nombre, e.nombre;
JOINs costosos para datos estáticos (autor, editorial)
COUNT sobre ejemplares en cada consulta
Datos cambian ≤5 veces/hora (oportunidad para cache)
⏱️ 1800ms
⏱️ 250ms
CREATE TABLE catalogo_libros (
libro_id INT PRIMARY KEY,
titulo VARCHAR(200),
autor_id INT,
autor_nombre VARCHAR(100),
editorial_id INT,
editorial_nombre VARCHAR(100),
copias_disponibles INT,
ultima_actualizacion TIMESTAMP
);
CREATE TRIGGER actualizar_catalogo
AFTER INSERT OR UPDATE ON libros
FOR EACH ROW
INSERT INTO catalogo_libros (
libro_id, titulo, autor_id, autor_nombre,
editorial_id, editorial_nombre, ultima_actualizacion
)
SELECT
l.id, l.titulo, l.autor_id, a.nombre,
l.editorial_id, e.nombre, NOW()
FROM libros l
JOIN autores a ON l.autor_id = a.id
JOIN editoriales e ON l.editorial_id = e.id
WHERE l.id = NEW.id
ON DUPLICATE KEY UPDATE
titulo = VALUES(titulo),
autor_nombre = VALUES(autor_nombre),
editorial_nombre = VALUES(editorial_nombre),
ultima_actualizacion = NOW();
-- Ejecutar cada 5 minutos
UPDATE catalogo_libros cl
JOIN (
SELECT libro_id, COUNT(*) AS disponibles
FROM ejemplares
WHERE disponible = true
GROUP BY libro_id
) ej ON cl.libro_id = ej.libro_id
SET cl.copias_disponibles = ej.disponibles,
cl.ultima_actualizacion = NOW();
SELECT l.titulo, a.nombre, e.nombre,
COUNT(ej.id) AS disponibles
FROM libros l
JOIN autores a ON l.autor_id = a.id
JOIN editoriales e ON l.editorial_id = e.id
LEFT JOIN ejemplares ej ON l.id = ej.libro_id
WHERE ej.disponible = true
GROUP BY l.id, a.nombre, e.nombre;
⏱️ 1800ms
SELECT
titulo,
autor_nombre,
editorial_nombre,
copias_disponibles
FROM catalogo_libros;
⏱️ 250ms
Tabla pedidos normalizada con múltiples JOINs para reportes:
-- Solución Propuesta
CREATE TABLE reporte_pedidos (
pedido_id INT PRIMARY KEY,
fecha DATE,
cliente_id INT,
cliente_nombre VARCHAR(100),
total_productos INT,
monto_total DECIMAL(10,2),
ultima_actualizacion TIMESTAMP
);
-- Actualización con:
-- 1. Trigger al crear pedido
-- 2. Job nocturno para recálculos
-- 3. Eventos para clientes VIP
Métricas Esperadas: