Modelo relacional y claves
Hasta ahora trabajabas con una sola tabla. Pero una empresa real se modela con muchas tablas relacionadas: los clientes en una, los productos en otra, los pedidos en otra… La relación se construye con dos tipos de clave.
1.1 · Clave primaria y clave foránea
productos.id)productos.categoria_id)En la tabla pedidos, la columna cliente_id es una clave foránea
hacia clientes.id: guarda qué cliente hizo ese pedido.
pedido_producto, con pedido_id + producto_id + cantidad.
Cada fila significa: «en el pedido X se pidieron N unidades del producto Y».1.2 · El esquema de tw_tienda
SHOW TABLES;
-- clientes (id, nombre, ciudad, email, creado)
-- categorias (id, nombre)
-- productos (id, nombre, categoria_id, precio, stock)
-- pedidos (id, cliente_id, fecha, estado)
-- pedido_producto (pedido_id, producto_id, cantidad)
DESCRIBE te muestra las claves: PRI en la columna de la
clave primaria y MUL si es clave foránea. Compruébalo en productos.INNER JOIN
INNER JOIN combina filas de dos tablas usando una condición de igualdad: cada fila de la tabla A se junta con las filas de la tabla B que cumplen la condición. Solo salen las que tienen pareja.
2.1 · Sintaxis básica
SELECT productos.nombre, categorias.nombre
FROM productos
INNER JOIN categorias
ON productos.categoria_id = categorias.id;
Se lee: «de productos, júntalo con categorías donde la categoría del producto coincida con el id de la
categoría». El ON es la condición del emparejamiento.
SELECT pedidos.id, clientes.nombre, pedidos.fecha
FROM pedidos
INNER JOIN clientes
ON pedidos.cliente_id = clientes.id;
tabla.columna cuando la columna exista en las dos tablas
(como id o nombre). Si no lo haces, MySQL devuelve «ambiguous column».2.2 · INNER JOIN + WHERE
Puedes filtrar después del JOIN con WHERE con total normalidad:
SELECT pedidos.id, clientes.nombre, pedidos.fecha
FROM pedidos
INNER JOIN clientes ON pedidos.cliente_id = clientes.id
WHERE pedidos.estado = 'entregado';
JOIN y INNER JOIN son lo mismo; INNER es
opcional. Acostúmbrate a escribirlo completo: queda explícita la intención.LEFT JOIN y RIGHT JOIN
LEFT JOIN mantiene todas las filas de la tabla de la izquierda del FROM,
y junta las de la tabla de la derecha que coincidan. Las que no tienen pareja aparecen con
NULL en las columnas de la derecha. Es ideal para encontrar lo que NO tiene relación.
3.1 · Sintaxis y uso típico
SELECT clientes.nombre, pedidos.id
FROM clientes
LEFT JOIN pedidos ON pedidos.cliente_id = clientes.id
WHERE pedidos.id IS NULL;
Si el pedido no existe, pedidos.id vale NULL: ese es que lo filtras. Este patrón
es oro: todas las filas de A sin pareja en B.
SELECT categorias.nombre AS categoria,
productos.nombre AS producto
FROM categorias
LEFT JOIN productos ON productos.categoria_id = categorias.id
WHERE productos.id IS NULL;
3.2 · INNER vs LEFT en una tabla
COUNT(columna_derecha) a secas:
primero filtra con WHERE columna IS NULL.Alias de tablas
Cuando cruzas varias tablas, escribir productos.nombre una y otra vez cansa. Con un
alias de tabla (AS o sin AS) quedan claves cortas:
SELECT p.nombre AS producto, c.nombre AS categoria
FROM productos p
INNER JOIN categorias c ON p.categoria_id = c.id;
El alias se define en el FROM/JOIN y se usa en el SELECT, WHERE, ON y ORDER BY.
No te preocupes por la colisión de nombres: cada alias es único.
SELECT p.id AS pedido, cl.nombre AS cliente, p.fecha
FROM pedidos p
INNER JOIN clientes cl ON p.cliente_id = cl.id
ORDER BY p.fecha DESC;
p de productos,
cl de clientes, pp de pedido_producto). Ningún alias con espacios.Varias tablas y relaciones N:M
Los JOIN se encadenan: cada uno une una tabla más. Para la relación N:M «pedido ↔ producto» cruzamos la
tabla puente pedido_producto por el medio.
5.1 · N:M: quién pidió qué
SELECT pe.id AS pedido, cl.nombre AS cliente, pro.nombre AS producto,
pp.cantidad
FROM pedidos pe
INNER JOIN clientes cl ON pe.cliente_id = cl.id
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id;
Se lee como una cadena: pedidos→clientes y pedidos→pedido_producto→productos. El orden importa poco para el resultado, pero mucho para la claridad.
5.2 · Consulta de resumen con cantidades
SELECT pe.id AS pedido,
pro.nombre AS producto,
pp.cantidad,
pro.precio,
pp.cantidad * pro.precio AS importe
FROM pedidos pe
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id;
5.3 · Unir hasta la categoría
SELECT pe.id AS pedido,
cl.nombre AS cliente,
pro.nombre AS producto,
cat.nombre AS categoria
FROM pedidos pe
INNER JOIN clientes cl ON pe.cliente_id = cl.id
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id
INNER JOIN categorias cat ON pro.categoria_id = cat.id;
ON con la condición correcta. Un error clásico
es unir por la columna equivocada y multiplicar los resultados (producto cartesiano).pedidos o
pedido_producto) y ve colgando las demás. Si el resultado se dispara, revisa el ON.✍️ Los 15 ejercicios del bloque
Abre MySQL Workbench, conecta y ejecuta sobre la base tw_tienda. Practica el cruce de tablas
hasta que los JOIN te salgan de memoria. Escribe las palabras clave en mayúsculas y añade un comentario
-- con el número del ejercicio.
mysql -u root -p < sql/sql-bloque3_tienda.sql.Sección A · INNER JOIN
1 Producto y su categoría
Une productos con categorias y muestra productos.nombre y
categorias.nombre.
Ver solución
-- Ejercicio 1: producto con su categoría
SELECT productos.nombre, categorias.nombre
FROM productos
INNER JOIN categorias ON productos.categoria_id = categorias.id;
2 Pedidos con su cliente
Une pedidos con clientes y muestra pedidos.id,
clientes.nombre y pedidos.fecha.
Ver solución
-- Ejercicio 2: pedidos con cliente
SELECT pedidos.id, clientes.nombre, pedidos.fecha
FROM pedidos
INNER JOIN clientes ON pedidos.cliente_id = clientes.id;
3 Solo entregados
Del ejercicio anterior, filtra con WHERE para quedarte con los pedidos en estado
'entregado'.
Ver solución
-- Ejercicio 3: pedidos entregados
SELECT pedidos.id, clientes.nombre, pedidos.fecha
FROM pedidos
INNER JOIN clientes ON pedidos.cliente_id = clientes.id
WHERE pedidos.estado = 'entregado';
4 Clientes madrileños con pedido
Consulta que cruce clientes y pedidos mostrando solo los clientes de
'Madrid'.
Ver solución
-- Ejercicio 4: clientes de Madrid con pedidos
SELECT clientes.nombre, clientes.ciudad, pedidos.id
FROM clientes
INNER JOIN pedidos ON pedidos.cliente_id = clientes.id
WHERE clientes.ciudad = 'Madrid';
Sección B · LEFT JOIN
5 Clientes sin pedidos
Con LEFT JOIN, encuentra qué clientes nunca han hecho un pedido
(el id del pedido es NULL).
Ver solución
-- Ejercicio 5: clientes sin pedidos
SELECT clientes.nombre
FROM clientes
LEFT JOIN pedidos ON pedidos.cliente_id = clientes.id
WHERE pedidos.id IS NULL;
6 Categorías sin productos
Con LEFT JOIN desde categorias, muestra las categorías que no tienen ningún
producto.
Ver solución
-- Ejercicio 6: categorías vacías
SELECT categorias.nombre
FROM categorias
LEFT JOIN productos ON productos.categoria_id = categorias.id
WHERE productos.id IS NULL;
Sección C · Alias
7 Alias de tablas
Repite el ejercicio 1 usando alias: p para productos y c para
categorías, con columnas alias producto y categoria.
Ver solución
-- Ejercicio 7: alias de tablas
SELECT p.nombre AS producto, c.nombre AS categoria
FROM productos p
INNER JOIN categorias c ON p.categoria_id = c.id;
8 Pedidos recientes primero
Consulta de pedidos con cliente (p y cl), ordenada por fecha
descendente.
Ver solución
-- Ejercicio 8: pedidos más recientes
SELECT p.id AS pedido, cl.nombre AS cliente, p.fecha
FROM pedidos p
INNER JOIN clientes cl ON p.cliente_id = cl.id
ORDER BY p.fecha DESC;
Sección D · Tres tablas y N:M
9 Quién pidió qué
Cruza pedidos, pedido_producto y productos. Muestra el
id del pedido, el nombre del producto y la cantidad.
Ver solución
-- Ejercicio 9: productos de cada pedido
SELECT pe.id AS pedido, pro.nombre AS producto, pp.cantidad
FROM pedidos pe
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id;
10 Cliente, pedido y producto
Al ejercicio 9 añade el clientes.nombre: cruza las 4 tablas (pedidos,
clientes, pedido_producto, productos).
Ver solución
-- Ejercicio 10: cliente + pedido + producto
SELECT cl.nombre AS cliente,
pe.id AS pedido,
pro.nombre AS producto,
pp.cantidad
FROM pedidos pe
INNER JOIN clientes cl ON pe.cliente_id = cl.id
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id;
11 Importe por línea
En la consulta de productos por pedido, añade pro.precio y una columna importe
calculada: pp.cantidad * pro.precio.
Ver solución
-- Ejercicio 11: importe de cada línea
SELECT pe.id AS pedido,
pro.nombre AS producto,
pp.cantidad,
pro.precio,
pp.cantidad * pro.precio AS importe
FROM pedidos pe
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id;
12 Categoría en cada línea
Cruza 5 tablas: pedidos, clientes, pedido_producto, productos y categorias. Muestra
pedido, cliente y categorias.nombre.
Ver solución
-- Ejercicio 12: con la categoría
SELECT pe.id AS pedido,
cl.nombre AS cliente,
pro.nombre AS producto,
cat.nombre AS categoria
FROM pedidos pe
INNER JOIN clientes cl ON pe.cliente_id = cl.id
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id
INNER JOIN categorias cat ON pro.categoria_id = cat.id;
Sección E · Combinando técnicas
13 Productos de un solo pedido
Usa LEFT JOIN desde productos para ver cuáles nunca se han pedido:
crúzalos con pedido_producto y filtra el que no tenga fila.
Ver solución
-- Ejercicio 13: productos nunca pedidos
SELECT pro.nombre AS producto
FROM productos pro
LEFT JOIN pedido_producto pp ON pp.producto_id = pro.id
WHERE pp.pedido_id IS NULL;
14 Pedidos pendientes con detalle
Consulta de pedido + cliente + producto donde el estado sea 'pendiente', con
WHERE después del JOIN.
Ver solución
-- Ejercicio 14: pedidos pendientes
SELECT pe.id AS pedido,
cl.nombre AS cliente,
pro.nombre AS producto
FROM pedidos pe
INNER JOIN clientes cl ON pe.cliente_id = cl.id
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id
WHERE pe.estado = 'pendiente';
15 El pedido más reciente con detalle
La misma consulta del ejercicio 12 pero ordenada por fecha descendente, mostrando también la fecha del pedido.
Ver solución
-- Ejercicio 15: detalle del pedido más reciente primero
SELECT pe.id AS pedido,
cl.nombre AS cliente,
pro.nombre AS producto,
pe.fecha
FROM pedidos pe
INNER JOIN clientes cl ON pe.cliente_id = cl.id
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id
ORDER BY pe.fecha DESC;
16 🏆 Proyecto integrador: mapa de ventas
Crea el fichero mapa_ventas.sql con una sola consulta que debería devolver
algo así como «qué cliente madrileño está comprando qué producto y de qué categoría». Requisitos:
- Cruza las 5 tablas con JOIN (clientes, pedidos, pedido_producto, productos, categorias).
- Usa alias de tablas en todas.
- Muestra
cliente,pedido,productoycategoriacomo columnas alias. - Filtra con
WHERE: clientes de'Madrid'. - Ordena por
pedidos.fechadescendente.
Ver una solución posible
-- Informe: mapa de ventas de clientes madrileños
SELECT cl.nombre AS cliente,
pe.id AS pedido,
pro.nombre AS producto,
cat.nombre AS categoria
FROM clientes cl
INNER JOIN pedidos pe ON pe.cliente_id = cl.id
INNER JOIN pedido_producto pp ON pp.pedido_id = pe.id
INNER JOIN productos pro ON pp.producto_id = pro.id
INNER JOIN categorias cat ON pro.categoria_id = cat.id
WHERE cl.ciudad = 'Madrid'
ORDER BY pe.fecha DESC;
📝 Examen del Bloque 3
Dos partes: un test teórico autoevaluable (mínimo 70 %) y un reto práctico de autocorrección con laboratorio.
Parte B · Reto «mapa de ventas»
Crea mapa_ventas.sql: una consulta que cruce las 5 tablas con alias y devuelva cliente,
pedido, producto y categoría para los clientes de Madrid, ordenada por fecha descendente. Tu SQL no se
ejecuta en el laboratorio: se valida con reglas automáticas. Mínimo para aprobar: 6 de 8
casillas.
Cumplidos: 0 / 8