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
- El modelo relacional en 10 minutos
- Clave primaria: natural frente a subrogada
- Claves candidatas y claves únicas
- Clave foránea e integridad referencial
- Qué pasa al borrar o modificar un padre: ON DELETE y ON UPDATE
- Cardinalidades: 1:1, 1:N y N:M
- Normalización práctica: 1FN, 2FN y 3FN
- Cuándo desnormalizar a propósito
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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
- 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.
- Los datos se relacionan por su valor, no por punteros. En TiendaVerde,
pedidos.cliente_id = 7señ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. - 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.
- 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 KEYLas razones:
- Uniformidad. Sabes que cualquier tabla se identifica por
id, y que cualquier clave foránea se llama<tabla_singular>_id. Cero sorpresas. - Estabilidad. El email de un cliente puede cambiar; su
idno. Si el email fuera la PK y cambiara, habría que actualizar en cascada todas las tablas que lo referencian. - Eficiencia. Un
INTEGERocupa 4 bytes; un email, 30 o 40. Todos los índices y todas las claves foráneas se benefician. - Legibilidad didáctica.
WHERE cliente_id = 7es infinitamente más cómodo en un curso queWHERE cliente_email = '[email protected]'.
Importante: usar
idsubrogado no exime de declarar la clave natural comoUNIQUE. En TiendaVerde,clientes.emailesUNIQUEaunque la PK seaid: sin eseUNIQUEpodrí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:
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.
- 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 |
Sí | 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? | Sí | Sí |
| Í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.
- 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_fkeysigue el patrón<tabla>_<columna>_fkeyy te dice exactamente cuál. - El
DETAILte 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:
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.
- 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 soloDELETEpuede 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), comolineas_pedidorespecto apedidos.
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.
- 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_tecnicacon textos enormes). - Aislar datos sensibles con permisos distintos (una tabla
empleados_datos_bancarios). - Especialización: una tabla
usuariosgeneral y tablasusuarios_admin/usuarios_clientecon 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 |
- 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:
fechadepende solo depedido_id, no del producto.cliente_nombre,cliente_emailycliente_ciudaddependen solo depedido_id.categoriadepende solo deproducto_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.
- 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 columnaproductos_idsviola la 1FN, impide las FK y hace las consultas imposibles. - Usar
CASCADEpor comodidad. UnDELETEpuede propagarse mucho más lejos de lo que crees. ReservaCASCADEpara 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
UNIQUEde la clave natural. Conidsubrogado como PK, nada impide duplicar el email de un cliente si no lo declarasUNIQUE. - 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
JOINy 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:
proveedoresyproductosclientesyresenaspedidosydevolucionesclientesyproductos(a través de reseñas)empleadosconsigo 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 | proveedores – productos |
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 | clientes – resenas |
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 | pedidos – devoluciones |
1:N | devoluciones.pedido_id |
CASCADE |
Un pedido puede tener varias devoluciones parciales. Una devolución no existe sin su pedido |
| 4 | clientes – productos |
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 | empleados – empleados |
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:
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:
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
idsubrogado en las nueve tablas por uniformidad, estabilidad y eficiencia, sin renunciar a declararUNIQUElas claves naturales comoclientes.email. - La clave foránea garantiza la integridad referencial: PostgreSQL rechaza con
violates foreign key constrainttanto 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:CASCADEparalineas_pedido,SET NULLparapedidos.empleado_id,RESTRICTpara 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 deempleados.jefe_idyclientes.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_unitarioconserve 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
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
