Hasta ahora hemos mirado las tablas de una en una. Pero el poder real de una base de datos relacional no está en las tablas: está en las relaciones entre ellas. Esta lección responde a la pregunta que todo principiante se hace al ver el esquema de TiendaVerde: "¿por qué nueve tablas y no una sola con todo?". Verás qué es una clave primaria y por qué el curso usa id numéricos, cómo una clave foránea impide físicamente que exista un pedido de un cliente inexistente, qué ocurre cuando intentas borrar un registro del que dependen otros, cómo se representan las cardinalidades 1:1, 1:N y N:M, y cómo la normalización descompone una tabla monolítica en un conjunto de tablas sanas. Es la lección más conceptual del módulo y también la que más rendimiento te dará cuando llegues a los JOIN.

Contenido

  1. El modelo relacional en 10 minutos
  2. Clave primaria: natural frente a subrogada
  3. Claves candidatas y claves únicas
  4. Clave foránea e integridad referencial
  5. Qué pasa al borrar o modificar un padre: ON DELETE y ON UPDATE
  6. Cardinalidades: 1:1, 1:N y N:M
  7. Normalización práctica: 1FN, 2FN y 3FN
  8. Cuándo desnormalizar a propósito
  9. Errores Comunes y Consejos
  10. Ejercicios
  11. Conclusión

  1. El modelo relacional en 10 minutos

El modelo relacional lo propuso Edgar F. Codd en 1970 y descansa en tres ideas sorprendentemente simples.

Relación, tupla y dominio

Concepto formal Nombre coloquial Qué es Ejemplo en TiendaVerde
Relación Tabla Un conjunto de tuplas con la misma estructura productos
Tupla Fila Un elemento concreto de ese conjunto El producto "Miel de azahar cruda 500 g"
Atributo Columna Una propiedad de la tupla precio
Dominio Tipo (más restricciones) El conjunto de valores válidos para un atributo NUMERIC(10,2) ≥ 0
Grado Número de columnas Cuántos atributos tiene la relación productos tiene grado 9
Cardinalidad Número de filas Cuántas tuplas contiene productos tiene 20 filas

Nota importante: "relación" no significa "relación entre tablas". En la terminología de Codd, una relación es una tabla. Las conexiones entre tablas se llaman asociaciones o, en la práctica, se implementan mediante claves foráneas. La coincidencia de nombres confunde a mucha gente.

Las tres propiedades que lo cambian todo

  1. Una relación es un conjunto, así que no hay orden ni duplicados conceptuales. Si dos filas fueran idénticas en todo, serían la misma tupla.
  2. Los datos se relacionan por su valor, no por punteros. En TiendaVerde, pedidos.cliente_id = 7 señala al cliente 7 porque el valor coincide, no porque haya una dirección de memoria guardada. Esta idea, que hoy parece obvia, era revolucionaria frente a los sistemas jerárquicos y de red de los años 60.
  3. La estructura es independiente del acceso. Puedes reorganizar índices y almacenamiento sin cambiar ni una consulta.

De la propiedad 2 sale directamente todo lo que verás en el módulo 3: un JOIN no es más que emparejar filas cuyos valores coinciden.

  1. Clave primaria: natural frente a subrogada

Una clave primaria (PK) es la columna —o combinación de columnas— que identifica de forma única cada fila de una tabla. Sus tres propiedades:

  • Única: no puede repetirse.
  • No nula: nunca puede ser NULL.
  • Estable: idealmente no debería cambiar nunca.

Hay dos filosofías para elegirla:

Tipo Qué es Ejemplo Ventajas Inconvenientes
Natural Un dato real del negocio que ya es único email en clientes, un ISBN, un NIF Significativa; sin columnas extra; evita duplicados por diseño Puede cambiar (una persona cambia de email); suele ser larga (texto), lo que encarece los índices y las claves foráneas
Subrogada Un identificador artificial sin significado id INTEGER autoincremental, UUID Corta, estable, uniforme, rápida en índices y JOIN; nunca cambia No significa nada; obliga a añadir un UNIQUE aparte para la clave real de negocio

Por qué este curso usa id subrogado en las nueve tablas

-- Todas las tablas de TiendaVerde siguen el mismo patrón
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY

Las razones:

  1. Uniformidad. Sabes que cualquier tabla se identifica por id, y que cualquier clave foránea se llama <tabla_singular>_id. Cero sorpresas.
  2. Estabilidad. El email de un cliente puede cambiar; su id no. Si el email fuera la PK y cambiara, habría que actualizar en cascada todas las tablas que lo referencian.
  3. Eficiencia. Un INTEGER ocupa 4 bytes; un email, 30 o 40. Todos los índices y todas las claves foráneas se benefician.
  4. Legibilidad didáctica. WHERE cliente_id = 7 es infinitamente más cómodo en un curso que WHERE cliente_email = '[email protected]'.

Importante: usar id subrogado no exime de declarar la clave natural como UNIQUE. En TiendaVerde, clientes.email es UNIQUE aunque la PK sea id: sin ese UNIQUE podrías registrar dos veces al mismo cliente y la base de datos no protestaría.

Clave primaria compuesta

Nada obliga a que la PK sea una sola columna. Podría formarse con varias:

-- Alternativa NO elegida para lineas_pedido
PRIMARY KEY (pedido_id, producto_id)

Eso significaría "un producto solo puede aparecer una vez en cada pedido". Es una decisión legítima, pero TiendaVerde usa un id propio en lineas_pedido por uniformidad y porque permite que un mismo producto aparezca en dos líneas del mismo pedido con precios o descuentos distintos.

  1. Claves candidatas y claves únicas

  • Una clave candidata es cualquier conjunto de columnas que identifica unívocamente una fila. Una tabla puede tener varias.
  • La clave primaria es la candidata que eliges como identificador oficial.
  • El resto de candidatas se declaran como claves únicas (UNIQUE).

En clientes tenemos dos candidatas:

Candidata ¿Elegida como PK? Cómo se declara
id PRIMARY KEY
email No UNIQUE

Diferencia clave entre PRIMARY KEY y UNIQUE:

Aspecto PRIMARY KEY UNIQUE
¿Admite NULL? No, nunca Sí (y en PostgreSQL, varios nulos a la vez)
¿Cuántas por tabla? Una Las que quieras
¿Puede ser destino de una FK?
Índice Se crea automáticamente Se crea automáticamente

Ese detalle de los NULL en UNIQUE sorprende: PostgreSQL considera que dos nulos no son iguales entre sí, así que una columna UNIQUE puede tener muchas filas con NULL. Si necesitas lo contrario, PostgreSQL 15 introdujo UNIQUE NULLS NOT DISTINCT.

  1. Clave foránea e integridad referencial

Una clave foránea (FK) es una columna que contiene valores que deben existir en la clave primaria de otra tabla. Es el mecanismo que conecta las tablas y, sobre todo, el que garantiza que no haya datos huérfanos.

En TiendaVerde:

-- Fragmento conceptual (la sintaxis completa es del módulo 5)
CREATE TABLE pedidos (
    id          INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    cliente_id  INTEGER NOT NULL REFERENCES clientes(id),
    empleado_id INTEGER     NULL REFERENCES empleados(id),
    ...
);

Se lee: "cliente_id debe corresponder a un id existente en clientes, y es obligatorio; empleado_id también debe existir en empleados, pero puede quedar vacío".

A esto se le llama integridad referencial: la base de datos garantiza que las referencias apuntan a algo real. Y no es una recomendación, es una barrera física.

Qué error da PostgreSQL al insertar una FK inexistente

TiendaVerde tiene 15 clientes. Si intentas registrar un pedido del cliente 999:

INSERT INTO pedidos (cliente_id, fecha_pedido, estado, metodo_pago, gastos_envio)
VALUES (999, '2026-03-01', 'pendiente', 'tarjeta', 4.95);
ERROR:  insert or update on table "pedidos" violates foreign key constraint "pedidos_cliente_id_fkey"
DETAIL:  Key (cliente_id)=(999) is not present in table "clientes".

Léelo con atención, porque lo verás muchas veces:

  • violates foreign key constraint → has roto una FK.
  • El nombre pedidos_cliente_id_fkey sigue el patrón <tabla>_<columna>_fkey y te dice exactamente cuál.
  • El DETAIL te da el valor culpable (999) y la tabla donde debería existir.

Sin esta restricción tendrías un pedido fantasma: aparecería en el total de ventas, pero al intentar mostrar el nombre del cliente no habría nada. Ese tipo de inconsistencia es demoledor en un sistema real, y es imposible de introducir aquí.

Qué error da al borrar un padre referenciado

El cliente 1 (Lucía Martínez Soler) tiene pedidos. Si intentas borrarlo:

DELETE FROM clientes WHERE id = 1;
ERROR:  update or delete on table "clientes" violates foreign key constraint "pedidos_cliente_id_fkey" on table "pedidos"
DETAIL:  Key (id)=(1) is still referenced from table "pedidos".

PostgreSQL se niega: borrar ese cliente dejaría pedidos apuntando al vacío. Este comportamiento por defecto se llama RESTRICT (técnicamente NO ACTION, que es equivalente salvo en transacciones diferidas), y es exactamente lo que quieres la mayoría de las veces.

  1. Qué pasa al borrar o modificar un padre: ON DELETE y ON UPDATE

Al declarar una FK puedes elegir qué debe ocurrir cuando la fila referenciada se borra (ON DELETE) o cambia su clave (ON UPDATE).

Acción Comportamiento al borrar el padre
NO ACTION (por defecto) Rechaza la operación con error
RESTRICT Rechaza inmediatamente, sin esperar al final de la transacción
CASCADE Borra también todas las filas hijas
SET NULL Deja la FK de las filas hijas a NULL (requiere que la columna admita nulos)
SET DEFAULT Pone el valor por defecto de la columna hija

Aplicado a TiendaVerde, la elección no es arbitraria: cada relación pide una acción distinta según el significado de negocio.

Relación Acción elegida Por qué
pedidos.cliente_id → clientes.id RESTRICT Un pedido no puede quedarse sin cliente. Antes de borrar un cliente hay que decidir qué hacer con su historial
pedidos.empleado_id → empleados.id SET NULL Si un comercial deja la empresa, el pedido sigue siendo válido: simplemente pasa a no tener comercial asignado, igual que los pedidos web
lineas_pedido.pedido_id → pedidos.id CASCADE Una línea no existe sin su pedido. Borrar el pedido debe llevarse sus líneas: son parte de él
lineas_pedido.producto_id → productos.id RESTRICT Nunca debes borrar un producto que se ha vendido: destruirías el histórico de facturación. Para retirarlo se usa activo = FALSE
clientes.referido_por_id → clientes.id SET NULL Si se borra quien refirió, el referido sigue siendo cliente; solo pierde esa información
empleados.jefe_id → empleados.id SET NULL Si un jefe se va, su equipo queda temporalmente sin jefe asignado, no se borra
resenas.producto_id → productos.id CASCADE Si el producto desapareciera del catálogo, sus reseñas no tienen sentido
devoluciones.pedido_id → pedidos.id CASCADE Una devolución es un hecho asociado a un pedido concreto

Regla mental para decidir: pregúntate "¿la fila hija tiene sentido por sí sola si el padre desaparece?". Si no lo tiene, CASCADE. Si lo tiene pero pierde una relación opcional, SET NULL. Si el padre no debería poder desaparecer estando referenciado, RESTRICT.

Cuidado con CASCADE. Es cómodo y peligroso: un solo DELETE puede propagarse por media base de datos en silencio. Úsalo únicamente cuando la relación sea de composición real (la parte no vive sin el todo), como lineas_pedido respecto a pedidos.

ON UPDATE funciona igual, pero se dispara cuando cambia la clave primaria del padre. Con claves subrogadas casi nunca se usa, porque un id autoincremental no cambia jamás. Es precisamente una de las ventajas de las claves subrogadas frente a las naturales.

La sintaxis completa para declarar estas restricciones (CONSTRAINT ... FOREIGN KEY ... REFERENCES ... ON DELETE CASCADE) se estudia en la lección 05-01. Aquí lo que importa es entender el criterio de decisión.

  1. Cardinalidades: 1:1, 1:N y N:M

La cardinalidad describe cuántas filas de una tabla pueden asociarse con cuántas de otra.

6.1. Uno a muchos (1:N)

Es la más frecuente. Una categoría tiene muchos productos; cada producto pertenece a una sola categoría.

erDiagram
    CATEGORIAS ||--o{ PRODUCTOS : "clasifica"
    CATEGORIAS {
        int id PK
        varchar nombre
        text descripcion
    }
    PRODUCTOS {
        int id PK
        varchar nombre
        int categoria_id FK
        numeric precio
        int stock
    }

Cómo se implementa: la FK va siempre en el lado "muchos". productos.categoria_id apunta a categorias.id. Nunca al revés: si pusieras una columna producto_id en categorias, solo cabría un producto por categoría.

Relaciones 1:N en TiendaVerde:

Lado "uno" Lado "muchos" Columna FK
categorias productos productos.categoria_id
proveedores productos productos.proveedor_id
clientes pedidos pedidos.cliente_id
empleados pedidos pedidos.empleado_id
pedidos lineas_pedido lineas_pedido.pedido_id
productos lineas_pedido lineas_pedido.producto_id
productos resenas resenas.producto_id
clientes resenas resenas.cliente_id
pedidos devoluciones devoluciones.pedido_id

6.2. Muchos a muchos (N:M) y la tabla puente

Un pedido contiene muchos productos, y un producto aparece en muchos pedidos. Una relación N:M no se puede implementar directamente: no hay dónde poner la FK. La solución es una tabla puente (o tabla intermedia, o de asociación).

erDiagram
    PEDIDOS ||--o{ LINEAS_PEDIDO : "contiene"
    PRODUCTOS ||--o{ LINEAS_PEDIDO : "aparece en"
    PEDIDOS {
        int id PK
        int cliente_id FK
        date fecha_pedido
        varchar estado
    }
    LINEAS_PEDIDO {
        int id PK
        int pedido_id FK
        int producto_id FK
        int cantidad
        numeric precio_unitario
        numeric descuento
    }
    PRODUCTOS {
        int id PK
        varchar nombre
        numeric precio
    }

La N:M entre pedidos y productos se descompone en dos relaciones 1:N que convergen en lineas_pedido.

Y aquí está el detalle que distingue una tabla puente bien diseñada: lineas_pedido no es solo un enlace, tiene datos propios.

Columna Por qué está ahí
cantidad ¿Cuántas unidades de ese producto en ese pedido? Solo tiene sentido en la intersección
precio_unitario El precio en el momento de la venta. Si el producto sube de precio mañana, las facturas antiguas no deben cambiar
descuento La rebaja aplicada a esa línea concreta

Ese precio_unitario es un ejemplo perfecto de desnormalización deliberada (sección 8): duplica información que también está en productos.precio, pero es imprescindible porque son cosas distintas: uno es el precio actual y otro el precio histórico facturado.

6.3. Uno a uno (1:1)

Cada fila de A se corresponde con como mucho una de B. Se implementa poniendo una FK en una de las dos tablas y declarándola además UNIQUE.

TiendaVerde no tiene relaciones 1:1, porque casi nunca hacen falta: si la correspondencia es exacta, lo natural es fundir ambas tablas en una. Los casos legítimos son tres:

  • Separar columnas grandes poco consultadas (una tabla productos_ficha_tecnica con textos enormes).
  • Aislar datos sensibles con permisos distintos (una tabla empleados_datos_bancarios).
  • Especialización: una tabla usuarios general y tablas usuarios_admin / usuarios_cliente con campos exclusivos.

6.4. Relaciones reflexivas

Una tabla puede referenciarse a sí misma. TiendaVerde tiene dos casos:

Relación Significado Cardinalidad
empleados.jefe_id → empleados.id Jerarquía organizativa 1:N (un jefe, muchos subordinados)
clientes.referido_por_id → clientes.id Programa de recomendación 1:N (un cliente refiere a varios)

Las dos columnas admiten NULL, y ese nulo tiene un significado preciso: jefe_id IS NULL identifica a la dirección general (la empleada 1, Rosa Alcázar Vives) y referido_por_id IS NULL identifica a los clientes que llegaron por su cuenta. Consultar estas relaciones requiere SELF JOIN, que se estudia en la lección 03-06.

6.5. Resumen

Cardinalidad Cómo se implementa Ejemplo en TiendaVerde
1:N FK en el lado "muchos" productos.categoria_id
N:M Tabla puente con dos FK lineas_pedido
1:1 FK con restricción UNIQUE (no aplica)
Reflexiva FK a la propia tabla, normalmente nulable empleados.jefe_id

  1. Normalización práctica: 1FN, 2FN y 3FN

La normalización es el proceso de organizar las columnas en tablas para eliminar redundancia y evitar anomalías. Suena académico, pero se entiende mejor viendo qué pasa cuando no se hace.

El punto de partida: una tabla desnormalizada

Imagina que TiendaVerde guardara todos sus pedidos en una única tabla:

pedido_id fecha cliente_nombre cliente_email cliente_ciudad productos categoria precio_total
1 2025-03-04 Lucía Martínez [email protected] Valencia Aceite oliva, Arroz integral, Infusión manzanilla Alimentación, Alimentación, Bebidas 43.20
5 2025-05-07 Lucía Martínez [email protected] Valencia Detergente eco, Luffa, Bolsas algodón Hogar sostenible 32.10
8 2025-06-28 Sofia Moreira [email protected] Lisboa Aceite oliva, Miel azahar, Infusión manzanilla Alimentación 66.87

Los problemas son inmediatos:

Anomalía Qué ocurre aquí
De inserción No puedes dar de alta un cliente que aún no ha pedido nada, ni un producto que aún no se ha vendido
De actualización Si Lucía cambia de email, hay que modificarlo en todas sus filas. Si fallas una, tendrás dos emails contradictorios
De borrado Si borras el pedido 8, pierdes toda la información de Sofia Moreira
De redundancia Los datos de Lucía se repiten en cada pedido: desperdicio de espacio y fuente constante de incoherencias
De consulta ¿Cuántas unidades de "Aceite oliva" se han vendido? Imposible: está dentro de una lista separada por comas

Primera forma normal (1FN)

Regla: cada celda contiene un único valor atómico; no hay grupos repetidos.

La columna productos viola la 1FN descaradamente: contiene tres valores en una celda. Para arreglarlo hay que sacar los productos a filas propias.

Señales de que algo viola la 1FN:

  • Listas separadas por comas en una celda.
  • Columnas numeradas: producto_1, producto_2, producto_3.
  • Un campo que a veces contiene un dato y a veces varios.

Tras aplicar 1FN, cada producto de cada pedido es una fila, lo que ya nos da el germen de lineas_pedido.

Segunda forma normal (2FN)

Regla: estar en 1FN y que ningún atributo no clave dependa solo de parte de una clave primaria compuesta.

Tras la 1FN, la clave de nuestra tabla sería (pedido_id, producto_nombre). Pero fíjate:

  • fecha depende solo de pedido_id, no del producto.
  • cliente_nombre, cliente_email y cliente_ciudad dependen solo de pedido_id.
  • categoria depende solo de producto_nombre, no del pedido.

Son dependencias parciales, y provocan que esos datos se repitan una vez por cada línea del pedido. La solución es dividir:

  • Lo que depende del pedido → tabla pedidos.
  • Lo que depende del producto → tabla productos.
  • Lo que depende de la combinación (cantidad, precio de venta, descuento) → tabla lineas_pedido.

Ahí tienes, deducida, la tabla puente de la sección 6.2.

Tercera forma normal (3FN)

Regla: estar en 2FN y que ningún atributo no clave dependa de otro atributo no clave (nada de dependencias transitivas).

En la tabla pedidos resultante seguiríamos teniendo cliente_nombre, cliente_email y cliente_ciudad. Estos dependen de cliente_email (o del cliente en general), no de pedido_id. Es una dependencia transitiva: pedido_id → cliente → email.

La solución: extraer una tabla clientes y dejar en pedidos solo la referencia cliente_id. Idénticamente, categoria en productos depende de la categoría, no del producto: se extrae categorias y queda productos.categoria_id.

El resultado

graph LR
    A["Tabla única<br/>desnormalizada"] -->|1FN: valores atómicos| B["pedidos_lineas<br/>una fila por producto"]
    B -->|2FN: separar dependencias parciales| C["pedidos + productos<br/>+ lineas_pedido"]
    C -->|3FN: eliminar transitivas| D["+ clientes + categorias<br/>+ proveedores…"]

Aplicando estas tres reglas al caso de TiendaVerde llegas, casi mecánicamente, al esquema de nueve tablas del curso. Esa es la respuesta a la pregunta inicial: el esquema no está partido por capricho, sino porque cada tabla agrupa exactamente los datos que dependen de una misma cosa.

Resumen memorizable de las tres formas normales:

Forma Regla en una frase Violación típica
1FN Un valor por celda, sin grupos repetidos "Aceite, Arroz, Infusión" en una columna
2FN Sin dependencias parciales de una clave compuesta fecha_pedido repetida en cada línea
3FN Sin dependencias transitivas entre atributos no clave cliente_email dentro de pedidos

La regla mnemotécnica clásica: "cada atributo no clave debe depender de la clave, de toda la clave y de nada más que la clave". La primera parte es 1FN/2FN, "de toda la clave" es 2FN y "de nada más que la clave" es 3FN.

Existen formas normales superiores (BCNF, 4FN, 5FN) que resuelven casos más raros. En la práctica profesional, llegar a 3FN cubre el 95 % de los diseños.

  1. Cuándo desnormalizar a propósito

La normalización optimiza la integridad y la escritura. A veces se paga un precio en velocidad de lectura, porque reconstruir una factura obliga a combinar cinco tablas. Desnormalizar es introducir redundancia conscientemente a cambio de rendimiento o de corrección histórica.

Casos legítimos, con ejemplos de TiendaVerde:

Caso Ejemplo Por qué está justificado
Datos históricos inmutables lineas_pedido.precio_unitario El precio facturado no debe cambiar cuando cambie productos.precio. No es redundancia: son datos distintos
Agregados precalculados Una columna pedidos.total Evita recalcular la suma de líneas en cada consulta. Coste: hay que mantenerla sincronizada (con triggers, módulo 10)
Copia de un atributo muy consultado Guardar cliente_pais en pedidos Evita un JOIN en informes que agrupan por país. Solo si el volumen lo justifica
Tablas de informes Una tabla resumen de ventas mensuales Los almacenes de datos usan esquemas en estrella deliberadamente desnormalizados

Y la regla de oro:

Normaliza primero. Desnormaliza después, con medidas en la mano, y documenta por qué.

Desnormalizar sin medir es la causa número uno de bases de datos incoherentes. Cada dato duplicado es un dato que puede quedar desincronizado, y necesitarás un mecanismo explícito (trigger, proceso batch, lógica de aplicación) para mantenerlo al día. Antes de desnormalizar, prueba con un índice (módulo 8) o una vista materializada (módulo 10): suelen resolver el problema sin coste de integridad.

Errores Comunes y Consejos

  • Confundir "relación" con "relación entre tablas". En el modelo de Codd, una relación es una tabla.
  • Poner la FK en el lado equivocado de una 1:N. Siempre va en el lado "muchos". Si la pones en el "uno", limitas la relación a un único hijo.
  • Intentar hacer una N:M sin tabla puente. Guardar "3,7,12" en una columna productos_ids viola la 1FN, impide las FK y hace las consultas imposibles.
  • Usar CASCADE por comodidad. Un DELETE puede propagarse mucho más lejos de lo que crees. Reserva CASCADE para relaciones de composición real.
  • Borrar productos vendidos. Destruye el histórico y salta la FK. Usa activo = FALSE (borrado lógico); por eso la columna existe.
  • Olvidar el UNIQUE de la clave natural. Con id subrogado como PK, nada impide duplicar el email de un cliente si no lo declaras UNIQUE.
  • Sobrenormalizar. Partir una tabla en siete por purismo académico complica cada consulta sin aportar integridad real.
  • Desnormalizar "por si acaso". Sin una medición que lo justifique, solo estás creando incoherencias futuras.
  • Consejo: indexa tus claves foráneas. PostgreSQL crea índice automáticamente para la PK, pero no para las FK. Sin ese índice, los JOIN y los borrados en cascada pueden ser muy lentos (módulo 8).
  • Consejo: dibuja el diagrama antes de escribir DDL. Diez minutos de esquema en papel evitan semanas de migraciones.
  • Consejo: nombra las FK con el patrón <tabla_singular>_id. cliente_id, producto_id, pedido_id. La consistencia hace que las consultas se escriban casi solas.

Ejercicios

Ejercicio 1

Para cada par de tablas de TiendaVerde, indica la cardinalidad (1:1, 1:N o N:M), dónde va la clave foránea y qué acción ON DELETE elegirías, justificándola:

  1. proveedores y productos
  2. clientes y resenas
  3. pedidos y devoluciones
  4. clientes y productos (a través de reseñas)
  5. empleados consigo misma

Ejercicio 2

Esta tabla viola las tres formas normales. Identifica qué regla rompe en cada caso y descomponla en tablas normalizadas hasta 3FN, indicando claves primarias y foráneas.

resena_id producto precio_producto categoria cliente_email cliente_ciudad puntuaciones fechas
1 Aceite de oliva 12.50 Alimentación [email protected] Valencia 5, 4 2025-03-15, 2025-04-02
2 Crema aloe vera 18.90 Cosmética natural [email protected] Valencia 5 2025-03-25

Ejercicio 3

Predice qué responde PostgreSQL a cada una de estas operaciones sobre la base de TiendaVerde ya cargada, y explica por qué:

-- a)
INSERT INTO resenas (producto_id, cliente_id, puntuacion, comentario, fecha)
VALUES (99, 1, 5, 'Excelente', '2026-03-01');

-- b)
DELETE FROM productos WHERE id = 1;

-- c)
DELETE FROM pedidos WHERE id = 20;

-- d)
UPDATE empleados SET jefe_id = NULL WHERE id = 4;

Soluciones

Solución 1

# Par Cardinalidad Dónde va la FK ON DELETE Justificación
1 proveedoresproductos 1:N productos.proveedor_id RESTRICT Un proveedor sirve muchos productos. No debes borrar un proveedor cuyos productos siguen en catálogo; se marca activo = FALSE
2 clientesresenas 1:N resenas.cliente_id CASCADE Un cliente escribe muchas reseñas. Si se ejerce el derecho de supresión del cliente, sus reseñas deben irse con él
3 pedidosdevoluciones 1:N devoluciones.pedido_id CASCADE Un pedido puede tener varias devoluciones parciales. Una devolución no existe sin su pedido
4 clientesproductos N:M Tabla puente resenas (con cliente_id y producto_id) Según cada lado Un cliente reseña muchos productos y un producto recibe muchas reseñas. resenas es tabla puente con datos propios: puntuación, comentario y fecha
5 empleadosempleados 1:N reflexiva empleados.jefe_id SET NULL Un jefe tiene varios subordinados. Si el jefe deja la empresa, el equipo queda sin jefe asignado pero no se borra

Solución 2

Violaciones:

Forma Qué se rompe
1FN puntuaciones y fechas contienen listas separadas por comas
2FN Tras separar cada reseña en su fila, precio_producto y categoria dependen solo del producto, no de la reseña
3FN cliente_ciudad depende de cliente_email (del cliente), no de resena_id; y categoria es una entidad propia, no un atributo del producto

Descomposición en 3FN:

categorias(id PK, nombre)
productos(id PK, nombre, precio, categoria_id FK → categorias.id)
clientes(id PK, email UNIQUE, ciudad)
resenas(id PK, producto_id FK → productos.id, cliente_id FK → clientes.id,
        puntuacion, fecha)

Y así queda:

Tabla Filas resultantes
categorias Alimentación, Cosmética natural
productos Aceite de oliva (12.50, Alimentación), Crema aloe vera (18.90, Cosmética natural)
clientes lucia.martinez@… (Valencia), carlos.ferrer@… (Valencia)
resenas 3 filas: (Aceite, Lucía, 5, 2025-03-15), (Aceite, Lucía, 4, 2025-04-02), (Crema, Carlos, 5, 2025-03-25)

Fíjate en que la lista "5, 4" de la primera fila se convierte en dos reseñas distintas: la 1FN nos obligó a descubrir que allí había realmente dos hechos, no uno.

Solución 3

a) Falla:

ERROR:  insert or update on table "resenas" violates foreign key constraint "resenas_producto_id_fkey"
DETAIL:  Key (producto_id)=(99) is not present in table "productos".

TiendaVerde tiene 20 productos, así que el 99 no existe. La integridad referencial impide crear una reseña huérfana.

b) Falla:

ERROR:  update or delete on table "productos" violates foreign key constraint "lineas_pedido_producto_id_fkey" on table "lineas_pedido"
DETAIL:  Key (id)=(1) is still referenced from table "lineas_pedido".

El producto 1 (Aceite de oliva virgen extra) aparece en varias líneas de pedido, y esa FK está declarada RESTRICT precisamente para proteger el histórico de facturación. Para retirarlo del catálogo se hace UPDATE productos SET activo = FALSE WHERE id = 1;.

c) Funciona, y borra más de lo que parece:

DELETE 1

Se elimina el pedido 20 y, en cascada, sus dos líneas de pedido (lineas_pedido.pedido_id está declarada ON DELETE CASCADE). Es el ejemplo perfecto de por qué CASCADE debe usarse con cuidado: una sola sentencia ha borrado tres filas en dos tablas. Si el pedido tuviera devoluciones asociadas, también desaparecerían.

d) Funciona:

UPDATE 1

El empleado 4 (Óscar Peris Blasco, comercial) pasa a no tener jefe asignado. La columna jefe_id admite NULL por diseño, así que no se viola ninguna restricción. Ahora habría dos empleados con jefe_id IS NULL: la directora general (que lo es por naturaleza) y este comercial (que lo es por un cambio organizativo). Es un buen recordatorio de que NULL puede significar cosas distintas en filas distintas, y de por qué conviene documentar su semántica.

Conclusión

Esta lección explica el porqué del esquema que cargarás a continuación:

  • El modelo relacional organiza los datos en relaciones (tablas) de tuplas (filas) con atributos (columnas) sobre dominios (tipos), y conecta la información por valor, no por punteros: de ahí nacen los JOIN.
  • La clave primaria identifica cada fila de forma única, no nula y estable. TiendaVerde usa id subrogado en las nueve tablas por uniformidad, estabilidad y eficiencia, sin renunciar a declarar UNIQUE las claves naturales como clientes.email.
  • La clave foránea garantiza la integridad referencial: PostgreSQL rechaza con violates foreign key constraint tanto insertar una referencia inexistente como borrar un padre referenciado.
  • Las acciones ON DELETE (RESTRICT, CASCADE, SET NULL) se eligen según el significado de negocio: CASCADE para lineas_pedido, SET NULL para pedidos.empleado_id, RESTRICT para productos vendidos.
  • Las cardinalidades 1:N (FK en el lado "muchos"), N:M (tabla puente como lineas_pedido, con datos propios) y las relaciones reflexivas de empleados.jefe_id y clientes.referido_por_id.
  • La normalización hasta 3FN, deducida a partir de una tabla de pedidos monolítica, explica por qué el esquema tiene nueve tablas; y la desnormalización deliberada justifica que lineas_pedido.precio_unitario conserve el precio histórico.

En la siguiente lección, La base de datos del curso: TiendaVerde, todo esto se hace tangible: verás el diagrama entidad-relación completo, la descripción tabla por tabla con sus columnas y tipos, y el script SQL listo para copiar que crea las nueve tablas y carga los datos que usarás durante los once módulos restantes. Al terminarla tendrás la base de datos funcionando en tu equipo, y a partir del módulo 2 empezarás a consultarla de verdad.

Curso de SQL

Módulo 1: Introducción a SQL

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados