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:

  1. Olvidar entidades (descubrís a mitad del proyecto que faltan los proveedores).
  2. Mezclar conceptos (cliente y empleado en una sola tabla, después que es desastre).
  3. 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:

  1. categoria es auto-referente: permite jerarquías arbitrariamente profundas.
  2. cliente_id y empleado_id en pedido son opcionales (NULL en pedidos sin cliente registrado o sin empleado asignado).
  3. estado con CHECK enumerado es más simple que tabla aparte.
  4. PedidoLinea y CompraLinea son entidades débiles: la PK incluye la del padre.
  5. ON DELETE CASCADE en 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.

✏️ 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.

✏️ 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)
);

3.10 Para profundizar


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.