JOIN en SQL
En esta lección
Hasta ahora cada consulta leía una tabla. Pero la información útil suele estar repartida: los pedidos guardan cliente_id, y el nombre del cliente está en otra tabla. JOIN combina tablas usando la columna que las relaciona. Es, probablemente, la parte de SQL que más usarás en un proyecto real.
En esta lección verás por qué los datos se reparten en varias tablas, qué hace la base de datos por dentro al combinarlas, y los dos tipos de JOIN que resuelven casi todo: INNER JOIN y LEFT JOIN.
Por qué hace falta JOIN
¿No sería más fácil guardar el nombre y la ciudad del cliente dentro de cada pedido? A primera vista sí, pero Ana tiene dos pedidos, así que su nombre y su ciudad estarían escritos dos veces. Si se muda a Bilbao, tendrías que cambiarlo en todos sus pedidos, y si se te olvida uno, la base de datos tendrá dos ciudades distintas para la misma persona.
Por eso una base de datos bien diseñada guarda cada dato una sola vez: los clientes en su tabla, los pedidos en la suya, y en cada pedido solo el id del cliente (la clave foránea). El precio de este orden es que, para ver el nombre junto al pedido, tienes que volver a juntar las tablas al consultar. Eso es un JOIN.
pedidos clientes
pedido 1 (cliente_id 1) ──────► 1 Ana García Santander
pedido 2 (cliente_id 3) ──────► 3 Marta Ruiz Santander
pedido 3 (cliente_id 1) ──────► 1 Ana García Santander
pedido 4 (cliente_id 2) ──────► 2 Luis Pérez Bilbao
pedido 5 (cliente_id 5) ──────► 5 Eva Martín Madrid
4 Pablo Díaz Madrid (ningún pedido)
Cada pedido “apunta” a su cliente. Ana recibe dos flechas y Pablo ninguna: no ha comprado nada.
El producto cartesiano
Para entender el JOIN, mira qué pasa si pones dos tablas en el FROM sin decir cómo se relacionan:
SELECT COUNT(*) AS combinaciones
FROM clientes, pedidos;
| combinaciones |
|---|
| 25 |
5 clientes × 5 pedidos = 25 filas. La base de datos ha combinado cada cliente con cada pedido. Es lo que se llama producto cartesiano. Estas son las primeras filas:
SELECT c.id, c.nombre, p.id AS pedido, p.cliente_id
FROM clientes c, pedidos p
ORDER BY c.id, p.id
LIMIT 6;
| id | nombre | pedido | cliente_id |
|---|---|---|---|
| 1 | Ana García | 1 | 1 |
| 1 | Ana García | 2 | 3 |
| 1 | Ana García | 3 | 1 |
| 1 | Ana García | 4 | 2 |
| 1 | Ana García | 5 | 5 |
| 2 | Luis Pérez | 1 | 1 |
La mayoría de estas combinaciones no tienen sentido: el pedido 2 es de Marta, no de Ana. Solo valen las filas donde cliente_id coincide con id. Un JOIN es exactamente eso: combinar y quedarse solo con las parejas que encajan.
Antiguamente se escribía así, filtrando con WHERE:
SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes c, pedidos p
WHERE p.cliente_id = c.id;
Funciona y aún lo verás en código antiguo, pero la forma moderna con JOIN ... ON separa la relación entre tablas (en el ON) de los filtros (en el WHERE), y es más difícil olvidarse de la condición. Con dos tablas de 10 000 filas, olvidarla genera 100 millones de combinaciones. Si alguna vez quieres todas las combinaciones, existe CROSS JOIN, que verás en más JOIN.
INNER JOIN
SELECT clientes.nombre, pedidos.fecha, pedidos.importe
FROM pedidos
INNER JOIN clientes ON pedidos.cliente_id = clientes.id;
| nombre | fecha | importe |
|---|---|---|
| Ana García | 2026-09-01 | 45.00 |
| Marta Ruiz | 2026-09-03 | 120.50 |
| Ana García | 2026-09-10 | 12.99 |
| Luis Pérez | 2026-09-12 | 60.00 |
| Eva Martín | 2026-09-15 | 33.25 |
JOIN clientesindica la segunda tabla.ONdice cómo se emparejan las filas: elcliente_iddel pedido con eliddel cliente.- Como las dos tablas tienen una columna
id, se escribetabla.columnapara no confundirlas.
INNER JOIN devuelve solo las filas que tienen pareja en las dos tablas. Pablo no ha hecho ningún pedido, así que no aparece. Escribir solo JOIN es lo mismo que INNER JOIN.
Alias de tabla
Para no escribir los nombres completos una y otra vez, dale a cada tabla un alias corto:
SELECT c.nombre, p.fecha, p.importe
FROM pedidos p
JOIN clientes c ON p.cliente_id = c.id
WHERE c.ciudad = 'Santander'
ORDER BY p.fecha;
| nombre | fecha | importe |
|---|---|---|
| Ana García | 2026-09-01 | 45.00 |
| Marta Ruiz | 2026-09-03 | 120.50 |
| Ana García | 2026-09-10 | 12.99 |
Después del JOIN, la consulta sigue como siempre: WHERE, ORDER BY, GROUP BY… trabajan sobre las filas ya combinadas, y puedes filtrar por columnas de cualquiera de las dos tablas.
Condiciones extra en el ON
El ON admite más de una condición con AND. Por ejemplo, clientes con sus pedidos de más de 50 €:
SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id AND p.importe > 50;
| nombre | pedido | importe |
|---|---|---|
| Marta Ruiz | 2 | 120.50 |
| Luis Pérez | 4 | 60.00 |
Con INNER JOIN da igual poner p.importe > 50 en el ON o en el WHERE: el resultado es el mismo. Por claridad, lo habitual es dejar en el ON la relación entre tablas y en el WHERE los filtros. Con LEFT JOIN, en cambio, no da igual, como verás enseguida.
LEFT JOIN
Devuelve todas las filas de la tabla de la izquierda (la del FROM), tengan o no pareja. Si no la tienen, las columnas de la otra tabla salen como NULL:
SELECT c.nombre, p.importe
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
ORDER BY c.id, p.id;
| nombre | importe |
|---|---|
| Ana García | 45.00 |
| Ana García | 12.99 |
| Luis Pérez | 60.00 |
| Marta Ruiz | 120.50 |
| Pablo Díaz | NULL |
| Eva Martín | 33.25 |
Ahora sí aparece Pablo, sin importe. LEFT JOIN también se puede escribir LEFT OUTER JOIN; es lo mismo.
Encontrar lo que falta: clientes sin pedidos
Como los clientes sin pareja llevan NULL en las columnas de pedidos, basta con quedarse con esas filas:
-- Clientes que nunca han comprado
SELECT c.nombre
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE p.id IS NULL;
| nombre |
|---|
| Pablo Díaz |
Es un patrón muy útil: productos que nunca se han vendido, alumnos sin matrícula, socios que no han venido este mes. Comprueba siempre una columna que nunca sea NULL cuando hay pareja, como la clave primaria (p.id); si miraras una columna que admite NULL, confundirías “sin pedido” con “pedido sin ese dato”. Recuerda que se escribe IS NULL, no = NULL (lo viste en valores NULL).
La condición en el ON o en el WHERE
Queremos todos los clientes y, al lado, sus pedidos de más de 50 € si los tienen. Compara estas dos consultas:
-- A: condición en el ON
SELECT c.nombre, p.importe
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id AND p.importe > 50;
| nombre | importe |
|---|---|
| Ana García | NULL |
| Luis Pérez | 60.00 |
| Marta Ruiz | 120.50 |
| Pablo Díaz | NULL |
| Eva Martín | NULL |
-- B: condición en el WHERE
SELECT c.nombre, p.importe
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE p.importe > 50;
| nombre | importe |
|---|---|
| Marta Ruiz | 120.50 |
| Luis Pérez | 60.00 |
En A, la condición decide qué pedidos se emparejan, pero todos los clientes se conservan. En B, primero se hace el LEFT JOIN completo y después el WHERE elimina las filas con importe NULL o menor de 50, así que el LEFT JOIN se ha convertido, en la práctica, en un INNER JOIN. Regla: con LEFT JOIN, las condiciones sobre la tabla de la derecha van en el ON.
JOIN + GROUP BY
Juntando lo que ya sabes de GROUP BY, el total gastado por cada cliente, con su nombre:
SELECT c.nombre, COUNT(p.id) AS pedidos, COALESCE(SUM(p.importe), 0) AS total
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre
ORDER BY total DESC;
| nombre | pedidos | total |
|---|---|---|
| Marta Ruiz | 1 | 120.50 |
| Luis Pérez | 1 | 60.00 |
| Ana García | 2 | 57.99 |
| Eva Martín | 1 | 33.25 |
| Pablo Díaz | 0 | 0 |
COUNT(p.id)cuenta solo los pedidos que existen, por eso Pablo tiene 0. ConCOUNT(*)contaría 1, por la fila conNULL.COALESCE(valor, 0)devuelve 0 cuando el valor esNULL.- Se agrupa por
c.idyc.nombre: el id distingue a dos clientes que se llamen igual.
JOIN de tres tablas
Un pedido puede tener varios productos. Eso se guarda en una tabla intermedia, lineas_pedido, que relaciona cada pedido con sus productos y la cantidad:
CREATE TABLE productos (
id INT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
precio DECIMAL(8, 2) NOT NULL
);
CREATE TABLE lineas_pedido (
pedido_id INT NOT NULL REFERENCES pedidos(id),
producto_id INT NOT NULL REFERENCES productos(id),
cantidad INT NOT NULL DEFAULT 1,
PRIMARY KEY (pedido_id, producto_id)
);
INSERT INTO productos VALUES
(1, 'Mosquetón', 7.50),
(2, 'Magnesio', 8.00),
(3, 'Cepillo de presas', 4.99),
(4, 'Arnés', 60.00),
(5, 'Casco', 60.50),
(6, 'Bolsa de magnesio', 17.25);
INSERT INTO lineas_pedido VALUES
(1, 1, 6),
(2, 4, 1), (2, 5, 1),
(3, 2, 1), (3, 3, 1),
(4, 4, 1),
(5, 6, 1), (5, 2, 2);
Para ver qué lleva cada pedido, se encadenan los JOIN. Cada uno añade una tabla y su ON la conecta con alguna de las anteriores:
SELECT p.id AS pedido, p.fecha, pr.nombre AS producto, l.cantidad, pr.precio
FROM pedidos p
JOIN lineas_pedido l ON l.pedido_id = p.id
JOIN productos pr ON pr.id = l.producto_id
ORDER BY p.id, pr.nombre;
| pedido | fecha | producto | cantidad | precio |
|---|---|---|---|---|
| 1 | 2026-09-01 | Mosquetón | 6 | 7.50 |
| 2 | 2026-09-03 | Arnés | 1 | 60.00 |
| 2 | 2026-09-03 | Casco | 1 | 60.50 |
| 3 | 2026-09-10 | Cepillo de presas | 1 | 4.99 |
| 3 | 2026-09-10 | Magnesio | 1 | 8.00 |
| 4 | 2026-09-12 | Arnés | 1 | 60.00 |
| 5 | 2026-09-15 | Bolsa de magnesio | 1 | 17.25 |
| 5 | 2026-09-15 | Magnesio | 2 | 8.00 |
pedidos ──(p.id = l.pedido_id)──► lineas_pedido ──(l.producto_id = pr.id)──► productos
Y se pueden añadir más. ¿Qué clientes han comprado magnesio? Hay que pasar por las cuatro tablas:
SELECT DISTINCT c.nombre
FROM clientes c
JOIN pedidos p ON p.cliente_id = c.id
JOIN lineas_pedido l ON l.pedido_id = p.id
JOIN productos pr ON pr.id = l.producto_id
WHERE pr.nombre = 'Magnesio';
| nombre |
|---|
| Ana García |
| Eva Martín |
El DISTINCT evita que un cliente salga repetido si hubiera comprado magnesio en varios pedidos.
USING: cuando las columnas se llaman igual
Si la columna que relaciona las dos tablas tiene el mismo nombre en ambas, puedes escribir USING (columna) en lugar del ON. Por ejemplo, una tabla de devoluciones que guarda el pedido y el producto devuelto:
CREATE TABLE devoluciones (
pedido_id INT NOT NULL,
producto_id INT NOT NULL,
motivo VARCHAR(100),
PRIMARY KEY (pedido_id, producto_id)
);
INSERT INTO devoluciones VALUES (2, 5, 'Talla pequeña'), (3, 3, 'Llegó roto');
SELECT pedido_id, producto_id, cantidad, motivo
FROM devoluciones
JOIN lineas_pedido USING (pedido_id, producto_id);
| pedido_id | producto_id | cantidad | motivo |
|---|---|---|---|
| 2 | 5 | 1 | Talla pequeña |
| 3 | 3 | 1 | Llegó roto |
USING (pedido_id, producto_id) equivale a ON devoluciones.pedido_id = lineas_pedido.pedido_id AND devoluciones.producto_id = lineas_pedido.producto_id. Además, las columnas del USING aparecen una sola vez y no hace falta indicar de qué tabla son. Funciona en MySQL, PostgreSQL y SQLite. Con clientes y pedidos no se puede usar, porque la columna se llama id en una y cliente_id en la otra.
Errores frecuentes
- Olvidar el
ON(o la condición delWHEREen la sintaxis antigua): obtienes el producto cartesiano, todas las combinaciones posibles. - Columnas ambiguas:
SELECT id FROM clientes JOIN pedidos ...da error (ambiguous column name: id), porque las dos tablas tienenid. Escribec.idop.id. - Emparejar las columnas equivocadas:
ON p.id = c.idno da error, pero empareja el pedido 1 con el cliente 1, el pedido 2 con el cliente 2… Datos falsos que parecen correctos. - Filtrar la tabla de la derecha de un
LEFT JOINen elWHERE: lo convierte en unINNER JOIN. - Usar
COUNT(*)tras unLEFT JOIN: cuenta la fila deNULL. UsaCOUNT(columna_de_la_derecha). - Sumar después de multiplicar filas: si unes
pedidosconlineas_pedido, cada pedido aparece una vez por línea. UnSUM(p.importe)contaría dos veces el pedido 2 (241.00 en lugar de 120.50). Suma los importes antes de unir, o suma las líneas (SUM(l.cantidad * pr.precio)).
Otros tipos de JOIN
INNER JOIN y LEFT JOIN resuelven la gran mayoría de consultas. Existen más:
RIGHT JOIN: comoLEFT JOIN, pero conserva todas las filas de la tabla de la derecha.FULL OUTER JOIN: conserva todas las filas de las dos tablas. No existe en MySQL.CROSS JOIN: el producto cartesiano, a propósito.- Self join: una tabla unida consigo misma, por ejemplo empleados y sus jefes.
Los tienes todos explicados, con ejemplos, en Más JOIN: RIGHT, FULL, CROSS y self join.
Resumen
| Quieres… | Escribe |
|---|---|
| Solo las filas con pareja en las dos tablas | FROM a JOIN b ON b.a_id = a.id |
| Todas las filas de la izquierda, con o sin pareja | FROM a LEFT JOIN b ON b.a_id = a.id |
| Las filas de la izquierda sin pareja | LEFT JOIN ... WHERE b.id IS NULL |
| Unir tres o más tablas | Un JOIN ... ON por cada tabla nueva |
| Unir por columnas con el mismo nombre | JOIN b USING (columna) |
| Contar hijos, incluidos los que tienen 0 | LEFT JOIN + COUNT(b.id) + GROUP BY |
JOINa secas esINNER JOIN.- Pon alias a las tablas y escribe siempre
alias.columna. - Con
LEFT JOIN, las condiciones sobre la tabla de la derecha van en elON.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.