🐬 Curso SQL · Nivel N3 Intermedio

Bloque 3: Combinar tablas con JOIN

El poder real de las bases relacionales está en poner tablas a trabajar juntas. Aprenderás el modelo relacional (claves primarias y foráneas, relaciones 1:N y N:M) y dominarás INNER JOIN, LEFT JOIN, los alias y las consultas que cruzan 3 o más tablas. Todo sobre una tienda con 5 tablas relacionadas.

⏱ ≈ 15 horas 📚 5 lecciones ✍️ 15 ejercicios guiados 📝 Examen del bloque + laboratorio
Lección 1 · Teoría

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

Clave primaria (PK) — identifica cada fila de forma única (ej. productos.id)
Clave foránea (FK) — columna que guarda la PK de otra tabla (ej. productos.categoria_id)
1 : N — un cliente tiene muchos pedidos; cada pedido pertenece a un cliente
N : M — un pedido tiene muchos productos y un producto aparece en muchos pedidos

En la tabla pedidos, la columna cliente_id es una clave foránea hacia clientes.id: guarda qué cliente hizo ese pedido.

💡 La relación N:M necesita una tabla puente. Aquí es 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

las 5 tablas de la 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)
⚠️ El DESCRIBE te muestra las claves: PRI en la columna de la clave primaria y MUL si es clave foránea. Compruébalo en productos.
🎉 Ya sabes de dónde salen las relaciones. Mueve las tablas juntas: INNER JOIN.
Lección 2 · Teoría

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

cada producto con su categoría
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.

pedidos con el nombre de su cliente
SELECT pedidos.id, clientes.nombre, pedidos.fecha
FROM   pedidos
INNER JOIN clientes
  ON pedidos.cliente_id = clientes.id;
⚠️ Usa el prefijo 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:

pedidos entregados con su cliente
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.
🎉 Ya emparejas tablas. Pero, ¿y las filas huérfanas? Ahí entra LEFT JOIN.
Lección 3 · Teoría

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

clientes que nunca han pedido
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.

categorías sin productos asignados
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

INNER JOIN — solo parejas: filas en ambas tablas
LEFT JOIN — todas las de la izquierda + parejas (o NULL)
RIGHT JOIN — todas las de la derecha (úsalo rara vez; existente por simetría)
Regla práctica — casi siempre quieres INNER o LEFT; RIGHT se evita por legibilidad
⚠️ Para contar «huérfanos» no uses COUNT(columna_derecha) a secas: primero filtra con WHERE columna IS NULL.
🎉 Encuentras huecos y lo que falta. Para que las consultas largas sean legibles: alias de tablas.
Lección 4 · Teoría

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:

alias p y c
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.

pedidos: cliente y estado, con alias
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;
💡 Convención: alias de 1 a 3 letras derivados del nombre (p de productos, cl de clientes, pp de pedido_producto). Ningún alias con espacios.
🎉 Consultas legibles. Ahora lo grande: cruzar 3 y 4 tablas, incluidas las relaciones N:M.
Lección 5 · Teoría

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é

pedidos, clientes y productos (3 joins)
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

importe de cada línea y total del pedido
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

4 tablas: qué categoría entra en cada pedido
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;
⚠️ Cada JOIN nuevo necesita su ON con la condición correcta. Un error clásico es unir por la columna equivocada y multiplicar los resultados (producto cartesiano).
💡 Comienza la consulta desde la tabla «central» (aquí pedidos o pedido_producto) y ve colgando las demás. Si el resultado se dispara, revisa el ON.
🎉 Bloque 3 completado: relacionas tablas, encuentras huérfanos y cruzas N:M. Ahora machaca los 15 ejercicios con la tienda. 🐬
Manos a la obra

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

📥 ¿No tienes la base de datos? Descárgala: sql/sql-bloque3_tienda.sql   y cárgala con 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, producto y categoria como columnas alias.
  • Filtra con WHERE: clientes de 'Madrid'.
  • Ordena por pedidos.fecha descendente.
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;
Evaluación del bloque

📝 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 A · Test teórico (aprueba con 11/15)

1 · ¿Qué identifica de forma única a una fila?
2 · ¿Qué guarda una clave foránea?
3 · ¿Qué devuelve un INNER JOIN?
4 · ¿Dónde se escribe la condición del emparejamiento?
5 · ¿Qué JOIN conserva todas las filas de la tabla de la izquierda?
6 · ¿Cómo se detecta que no hay pareja en un LEFT JOIN?
7 · ¿Para qué sirve un alias de tabla?
8 · Una relación N:M entre pedidos y productos necesita…
9 · ¿Qué significa 1:N entre clientes y pedidos?
10 · ¿Qué columna habría que usar para unir pedidos con clientes?
11 · ¿Cuándo aparece «ambiguous column» en MySQL?
12 · Un resultado con un producto cartesiano es síntoma de…
13 · ¿Dónde se define el alias de una tabla?
14 · ¿Cuántas tablas une la consulta: productos, pedido_producto, pedidos?
15 · ¿Qué JOIN usarías para ver TODOS los clientes aunque no tengan pedidos?

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

🎉 ¿Superaste las dos partes? Enhorabuena: ya combinas tablas como un profesional. En el bloque 4 descubrirás cómo resumir y agrupar datos con funciones de agregación, subconsultas y CTEs. 🐬🔥