Modelado entidad-relación
"Antes de escribir código, dibuja. Antes de escribir SQL, modela."
Qué vas a aprender en este capítulo
Las dos sesiones anteriores empezaron con el esquema ya hecho. En la realidad, el diseño es lo más difícil. Antes de escribir CREATE TABLE, hay que entender qué entidades viven en el sistema y cómo se relacionan. El modelo entidad-relación (ER) es la herramienta visual estándar para eso. Vas a aprender a identificar entidades, dibujar el diagrama, convertirlo a tablas, y a evitar los errores típicos del modelador novato.
3.1 La idea: dibujar antes de codificar
💡 Intuición
Imaginá que te piden hacer el sistema de la pupusería. Si saltás directo a CREATE TABLE, vas a:
- Olvidar entidades (descubrís a mitad del proyecto que faltan los proveedores).
- Mezclar conceptos (cliente y empleado en una sola tabla, después que es desastre).
- Modelar mal las relaciones (¿un pedido tiene un cliente, o varios? ¿un producto puede ir en varios pedidos? Sin pensarlo, podés errar).
El modelo ER te obliga a pensar antes de escribir.
El modelo ER es una abstracción visual que muestra qué cosas (entidades) hay en el sistema, qué propiedades tienen (atributos) y cómo se conectan (relaciones). Una vez claro el dibujo, traducir a SQL es mecánico.
Existe desde 1976 (Peter Chen) y sigue siendo el estándar. Tiene variantes notacionales (Crow's Foot, UML, Chen original), pero la idea es la misma.
📜 Historia
Peter Chen, profesor del MIT, publicó "The Entity-Relationship Model—Toward a Unified View of Data" en 1976. Su contribución: un lenguaje visual para modelar bases de datos, separado del modelo físico (cómo se almacenan).
Antes, el diseño era informal y dependía del DBMS específico. Chen propuso modelar primero conceptualmente, después implementar.
Su notación original (rectángulos para entidades, rombos para relaciones, óvalos para atributos) sigue usándose en libros, pero en la industria predomina Crow's Foot (más compacta) y UML class diagrams (extendido, también para programación).
Chen recibió el premio Achievement Award de la ACM en 2003. Su modelo fue tan influyente que es difícil pensar en bases de datos sin él.
3.2 Conceptos básicos
📐 Fundamento
Entidad
Una "cosa" del mundo real que querés representar — sustantivos: cliente, producto, pedido, empleado.
Propiedad clave: las entidades tienen identidad. Un cliente sigue siendo el mismo aunque cambien sus datos.
Atributo
Propiedad de una entidad. Cliente tiene nombre, email, teléfono.
Tipos:
- Simple: un valor (edad: 19).
- Compuesto: varios sub-atributos (dirección: calle + ciudad + CP).
- Multivaluado: varios valores (teléfonos: lista).
- Derivado: se calcula (edad a partir de fecha_nacimiento).
- Identificador (clave): identifica únicamente la entidad.
En el modelo relacional, los multivaluados y compuestos requieren descomposición (otra tabla). Lo veremos.
Relación
Asociación entre entidades — verbos: cliente hace pedido, producto aparece en pedido.
Tienen:
- Cardinalidad (cuántas instancias se relacionan).
- Participación (obligatoria u opcional).
Cardinalidad
Las cuatro relaciones básicas:
- 1:1. Una persona ↔ un pasaporte.
- 1:N. Un cliente hace muchos pedidos; cada pedido tiene un cliente.
- N:1. El reverso de 1:N.
- N:M. Un libro tiene muchos autores; un autor escribe muchos libros.
Notación Crow's Foot
|| → exactamente uno
)| → uno o más
o| → uno o ninguno
o) → cero o más
Una relación se dibuja como una línea entre las entidades, con notación a cada extremo:
Cliente ||---o)< Pedido [cliente hace 0 o más pedidos]
Cliente ||---|| Pasaporte [1 a 1]
(En texto plano es feo. En herramientas como dbdiagram.io, draw.io, MySQL Workbench, lo ves bonito.)
3.3 Pasos del modelado
🛠️ En la práctica
Receta para tomar requisitos y producir un diagrama ER:
Paso 1: identificá entidades
Releé la descripción del sistema. Todos los sustantivos importantes son candidatos:
"Una pupusería vende productos a clientes. Cada cliente puede hacer muchos pedidos. Cada pedido contiene varios productos con sus cantidades. Hay un inventario de los productos. Algunos clientes son fieles y tienen descuento."
Candidatos: cliente, pedido, producto, inventario, fidelidad.
Filtrar: algunos son atributos, no entidades. Inventario probablemente es un atributo stock del producto, no entidad. Fidelidad puede ser atributo del cliente (es_fiel: bool o nivel_fidelidad: int).
Paso 2: identificá atributos
Para cada entidad, listá qué propiedades tiene:
- Cliente: nombre, email, teléfono, fecha_registro.
- Producto: código, nombre, precio, stock, descripción.
- Pedido: id, fecha, total, cliente.
Cuál es la PK: elegí o crea un id. email puede ser clave candidata pero los emails cambian — un id artificial es más robusto.
Paso 3: identificá relaciones
Pares de entidades + verbo:
- Cliente hace Pedido (1:N).
- Pedido contiene Producto (N:M, con cantidad como atributo de la relación).
Paso 4: cardinalidad
¿Cuántas en cada lado?
- Un cliente puede tener 0 o más pedidos. Un pedido pertenece a 0 o 1 clientes (anónimo o no).
- Un pedido contiene 1 o más productos. Un producto aparece en 0 o más pedidos.
Paso 5: dibujá
Con las herramientas que tengas. No tiene que ser perfecto — se itera.
Paso 6: validá con stakeholders
Mostrále el diagrama al cliente, al usuario, a tu equipo. Frecuentemente descubrís entidades faltantes o relaciones mal entendidas.
3.4 Convertir ER a SQL
📐 Fundamento
Una vez claro el ER, traducir es mecánico:
Entidad → Tabla
Cada entidad se vuelve una tabla. Atributos simples → columnas. PK → PRIMARY KEY.
CREATE TABLE cliente (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE,
fecha_registro DATE NOT NULL DEFAULT CURRENT_DATE
);
Atributo compuesto → Múltiples columnas
Dirección (calle + ciudad + CP) →:
calle VARCHAR(200),
ciudad VARCHAR(100),
codigo_postal VARCHAR(10)
O una tabla aparte si la dirección es entidad propia (ej: cliente tiene varias direcciones).
Atributo multivaluado → Tabla aparte
Cliente con varios teléfonos →:
CREATE TABLE cliente_telefono (
cliente_id INTEGER REFERENCES cliente(id) ON DELETE CASCADE,
telefono VARCHAR(20),
PRIMARY KEY (cliente_id, telefono)
);
Relación 1:N → FK en el lado "muchos"
Cliente 1:N Pedido → poné cliente_id en pedido:
CREATE TABLE pedido (
id SERIAL PRIMARY KEY,
cliente_id INTEGER REFERENCES cliente(id),
fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Relación 1:1 → FK en cualquier lado (con UNIQUE)
CREATE TABLE pasaporte (
id SERIAL PRIMARY KEY,
persona_id INTEGER UNIQUE REFERENCES persona(id),
numero VARCHAR(20) UNIQUE NOT NULL,
expedido DATE
);
Relación N:M → Tabla intermedia
Pedido N:M Producto → tabla pedido_linea:
CREATE TABLE pedido_linea (
pedido_id INTEGER REFERENCES pedido(id) ON DELETE CASCADE,
producto VARCHAR(10) REFERENCES producto(codigo),
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
importe DECIMAL(10,2),
PRIMARY KEY (pedido_id, producto)
);
Atributos de la relación (cantidad, importe) van en la tabla intermedia.
Entidad débil → FK como parte de la PK
Una entidad débil es una que no existe por sí sola, depende de otra. Ejemplo: una "línea de pedido" no existe sin pedido.
CREATE TABLE pedido_linea (
pedido_id INTEGER REFERENCES pedido(id) ON DELETE CASCADE,
numero INTEGER, -- 1, 2, 3, ... dentro del pedido
PRIMARY KEY (pedido_id, numero) -- la PK incluye al padre
);
3.5 Patrones comunes de modelado
🛠️ En la práctica
Pattern 1: jerarquías (herencia)
Empleado → tiene subtipos: Gerente, Vendedor, Repartidor.
Tres estrategias:
A. Una tabla con type discriminator:
CREATE TABLE empleado (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100),
tipo VARCHAR(20) CHECK (tipo IN ('gerente', 'vendedor', 'repartidor')),
sueldo DECIMAL,
-- columnas específicas, posiblemente NULL si no aplican
bono_meta DECIMAL, -- solo vendedores
moto BOOLEAN -- solo repartidores
);
Ventaja: una sola tabla, queries simples. Desventaja: muchas columnas NULL, sin garantía de que solo apliquen a su tipo.
B. Una tabla por subtipo:
CREATE TABLE gerente (...);
CREATE TABLE vendedor (...);
CREATE TABLE repartidor (...);
Ventaja: sin NULL, schema preciso.
Desventaja: consultas que tocan "todos los empleados" requieren UNION.
C. Tabla padre + tablas hijo (más usada):
CREATE TABLE empleado (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100),
tipo VARCHAR(20)
);
CREATE TABLE vendedor (
empleado_id INTEGER PRIMARY KEY REFERENCES empleado(id),
bono_meta DECIMAL
);
CREATE TABLE repartidor (
empleado_id INTEGER PRIMARY KEY REFERENCES empleado(id),
moto BOOLEAN
);
Ventaja: datos comunes en padre, específicos en hijos. Desventaja: JOINs para obtener todo de un empleado.
Cuál usar depende del caso. Si los subtipos son muy distintos, separá. Si son muy similares con pocos campos extras, una sola tabla.
Pattern 2: árbol (auto-referencia)
Categorías de productos en árbol (electrónica → computadoras → laptops):
CREATE TABLE categoria (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100),
padre_id INTEGER REFERENCES categoria(id)
);
padre_id referencia la misma tabla. NULL para raíces.
Pattern 3: histórico / auditoría
Necesitás recordar cómo era un dato en cierto momento.
CREATE TABLE precio_historico (
producto VARCHAR(10) REFERENCES producto(codigo),
fecha DATE,
precio DECIMAL(10,2),
PRIMARY KEY (producto, fecha)
);
Cada cambio de precio insertás una fila. La tabla producto mantiene el actual, precio_historico el archivo.
Pattern 4: many-to-many con atributos
Más allá de la simple FK doble: la relación tiene su propia info.
-- Estudiante toma materia (con nota y semestre)
CREATE TABLE inscripcion (
estudiante_id INTEGER REFERENCES estudiante(id),
materia_id INTEGER REFERENCES materia(id),
semestre VARCHAR(10),
nota DECIMAL(3,1),
PRIMARY KEY (estudiante_id, materia_id, semestre)
);
La PK es triple: el mismo estudiante puede repetir la materia en otro semestre.
Pattern 5: soft delete
En vez de borrar, marcar como inactivo:
CREATE TABLE cliente (
id SERIAL PRIMARY KEY,
...
activo BOOLEAN NOT NULL DEFAULT TRUE,
eliminado_en TIMESTAMP
);
Ventaja: mantenés la historia, podés recuperar.
Desventaja: todas las queries tienen que filtrar WHERE activo = TRUE.
3.6 Errores típicos del novato
⚠️ Trampa común
1. Listas en columnas
-- MAL
productos VARCHAR(500) -- "queso:5,frijol:3"
Imposible de consultar sin parsear. Viola 1FN. Se corrige extrayendo a tabla aparte.
2. Datos de varios tipos en una sola columna
-- MAL
extra VARCHAR(200) -- a veces fecha, a veces número, a veces texto
Sin tipo claro = sin validación = bugs.
3. Olvidar la PK
Toda tabla debe tener PK. Sin ella, el DBMS no puede garantizar identidad.
4. Usar datos sensibles como PK
-- MAL
CREATE TABLE cliente (
email VARCHAR(100) PRIMARY KEY,
...
);
Si el cliente cambia email, todas las FK que apuntan a cliente quedan rotas. Mejor un id artificial.
5. No anticipar relaciones
"Por ahora un pedido tiene un cliente, no necesito tabla intermedia". Dos meses después: "ay, un pedido puede ser de varios clientes para regalos compartidos". Refactor pesado.
Defensa: modelá pensando en lo que podría llegar a pasar, no solo en lo de hoy.
6. Cardinalidad errada
Confundir 1:N con N:M es la trampa más común. Pensá: ¿el lado A tiene un solo B? ¿El lado B tiene un solo A? Las cuatro combinaciones dan los cuatro casos.
3.7 Proyecto: ER de la pupusería
🏗️ Avance del proyecto
Versión completa con varias entidades nuevas:
Entidades:
- Cliente (id, nombre, email, telefono, fecha_registro)
- Producto (codigo, nombre, descripcion, precio, stock, categoria)
- Categoria (id, nombre) — auto-referencia para subcategorías
- Pedido (id, fecha, cliente_id, total, estado)
- PedidoLinea (pedido_id, producto, cantidad, importe)
- Empleado (id, nombre, rol, sueldo)
- Proveedor (id, nombre, contacto)
- Compra (id, fecha, proveedor_id, total)
- CompraLinea (compra_id, producto, cantidad, costo_unitario)
Relaciones:
- Cliente 1:N Pedido
- Pedido N:M Producto (vía PedidoLinea, con cantidad e importe)
- Producto N:1 Categoria
- Categoria N:1 Categoria (auto-relación)
- Empleado 1:N Pedido (quién atendió)
- Proveedor 1:N Compra
- Compra N:M Producto (vía CompraLinea)
Esquema SQL:
CREATE TABLE categoria (
id SERIAL PRIMARY KEY,
nombre VARCHAR(50) NOT NULL,
padre_id INTEGER REFERENCES categoria(id)
);
CREATE TABLE producto (
codigo VARCHAR(10) PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
descripcion TEXT,
precio DECIMAL(10,2) NOT NULL CHECK (precio >= 0),
stock INTEGER NOT NULL DEFAULT 0,
categoria_id INTEGER REFERENCES categoria(id)
);
CREATE TABLE cliente (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE,
telefono VARCHAR(20),
fecha_registro DATE NOT NULL DEFAULT CURRENT_DATE
);
CREATE TABLE empleado (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
rol VARCHAR(20) CHECK (rol IN ('gerente', 'vendedor', 'cocinero', 'repartidor')),
sueldo DECIMAL(10,2)
);
CREATE TABLE pedido (
id SERIAL PRIMARY KEY,
fecha TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
cliente_id INTEGER REFERENCES cliente(id),
empleado_id INTEGER REFERENCES empleado(id),
estado VARCHAR(20) CHECK (estado IN ('pendiente', 'pagado', 'cancelado')),
total DECIMAL(10,2) NOT NULL DEFAULT 0
);
CREATE TABLE pedido_linea (
pedido_id INTEGER REFERENCES pedido(id) ON DELETE CASCADE,
producto VARCHAR(10) REFERENCES producto(codigo),
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
importe DECIMAL(10,2) NOT NULL,
PRIMARY KEY (pedido_id, producto)
);
CREATE TABLE proveedor (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
contacto VARCHAR(100)
);
CREATE TABLE compra (
id SERIAL PRIMARY KEY,
fecha DATE NOT NULL DEFAULT CURRENT_DATE,
proveedor_id INTEGER NOT NULL REFERENCES proveedor(id),
total DECIMAL(10,2) NOT NULL DEFAULT 0
);
CREATE TABLE compra_linea (
compra_id INTEGER REFERENCES compra(id) ON DELETE CASCADE,
producto VARCHAR(10) REFERENCES producto(codigo),
cantidad INTEGER NOT NULL CHECK (cantidad > 0),
costo_unitario DECIMAL(10,2),
PRIMARY KEY (compra_id, producto)
);
Decisiones de modelado documentadas:
categoriaes auto-referente: permite jerarquías arbitrariamente profundas.cliente_idyempleado_iden pedido son opcionales (NULL en pedidos sin cliente registrado o sin empleado asignado).estadocon CHECK enumerado es más simple que tabla aparte.- PedidoLinea y CompraLinea son entidades débiles: la PK incluye la del padre.
ON DELETE CASCADEen líneas: si borrás pedido o compra, sus líneas mueren. Prevé borrados accidentales con un soft-delete adicional si es necesario.
3.8 Resumen visual
| Concepto | Significado |
|---|---|
| Entidad | Sustantivo del dominio (cliente, producto) |
| Atributo | Propiedad (nombre, precio) |
| Relación | Asociación entre entidades |
| Cardinalidad | 1:1, 1:N, N:M |
| PK | Identificador único |
| FK | Referencia a otra entidad |
| Entidad débil | Depende de otra (línea de pedido) |
| Tabla intermedia | Para N:M |
3.9 Ejercicios
✏️ Ejercicio 3.1 — Identificar entidades
Para una biblioteca: identificá 4-5 entidades, sus atributos principales y cardinalidad de las relaciones.
Solución
Entidades:
- Libro: id, titulo, autor, isbn, año, ejemplares
- Socio: id, nombre, email, fecha_registro
- Prestamo: id, libro_id, socio_id, fecha_inicio, fecha_devolucion
- Empleado: id, nombre, rol
Relaciones:
- Socio 1:N Prestamo
- Libro 1:N Prestamo
- Empleado 1:N Prestamo (quién atendió)
(Si un libro tiene varios autores, autor sería entidad aparte y la relación libro↔autor sería N:M.)
✏️ Ejercicio 3.2 — Convertir ER a SQL
Suponé el ER:
- Curso (id, nombre, créditos)
- Estudiante (matricula, nombre, carrera)
- Inscripcion (estudiante↔curso, semestre, nota)
Escribí el SQL para crear las tres tablas.
Solución
CREATE TABLE curso (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
creditos INTEGER NOT NULL CHECK (creditos > 0)
);
CREATE TABLE estudiante (
matricula VARCHAR(20) PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
carrera VARCHAR(50)
);
CREATE TABLE inscripcion (
estudiante VARCHAR(20) REFERENCES estudiante(matricula),
curso_id INTEGER REFERENCES curso(id),
semestre VARCHAR(10),
nota DECIMAL(3,1),
PRIMARY KEY (estudiante, curso_id, semestre)
);
✏️ Ejercicio 3.3 — Detectar errores
Encontrá los problemas en este modelo:
CREATE TABLE empleados (
id INTEGER,
nombre VARCHAR(50),
contactos VARCHAR(500), -- "tel:55-12,email:x@y.com,addr:..."
jefe VARCHAR(50), -- nombre del jefe
sueldo DECIMAL,
fecha VARCHAR(20)
);
Solución
Errores:
- Sin PK declarada.
iddebería serPRIMARY KEY. contactosmezcla varios atributos en un string. Viola 1FN. Cada uno debería ser columna o tabla aparte.jefepor nombre rompe integridad referencial. Si dos empleados se llaman igual, ambiguo. Si renombran, se rompe. Debería serjefe_id INTEGER REFERENCES empleado(id).fechacomo VARCHAR — no se puede ordenar/comparar bien. UsáDATE.- Sin restricciones — sueldo puede ser negativo.
- Sin tipo de empleado ni rol — todos los empleados son iguales, no se distinguen.
Versión corregida:
CREATE TABLE empleado (
id SERIAL PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE,
telefono VARCHAR(20),
direccion VARCHAR(200),
jefe_id INTEGER REFERENCES empleado(id),
rol VARCHAR(20) NOT NULL,
sueldo DECIMAL(10,2) CHECK (sueldo > 0),
fecha_alta DATE NOT NULL
);
3.10 Para profundizar
- Toby Teorey, Database Modeling and Design. Texto clásico.
- dbdiagram.io — herramienta online para dibujar diagramas ER fácil.
- MySQL Workbench / DBeaver — clientes con modelador visual.
- Próximo capítulo: Normalización — las formas normales que garantizan que tu esquema esté bien diseñado.
Definiciones nuevas: entidad, atributo (simple, compuesto, multivaluado, derivado, identificador), relación, cardinalidad (1:1, 1:N, N:M), participación, entidad débil, jerarquía, auto-referencia, soft delete, modelo conceptual.