Saltar al contenido
join.sql · devschool

JOIN en SQL

Lección 10 de 22 · 14 min de lectura · Actualizado el

En esta lección
  1. Por qué hace falta JOIN
  2. El producto cartesiano
  3. INNER JOIN
  4. Alias de tabla
  5. LEFT JOIN
  6. JOIN + GROUP BY
  7. JOIN de tres tablas
  8. USING: cuando las columnas se llaman igual
  9. Errores frecuentes
  10. Otros tipos de JOIN
  11. Resumen

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;
idnombrepedidocliente_id
1Ana García11
1Ana García23
1Ana García31
1Ana García42
1Ana García55
2Luis Pérez11

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;
nombrefechaimporte
Ana García2026-09-0145.00
Marta Ruiz2026-09-03120.50
Ana García2026-09-1012.99
Luis Pérez2026-09-1260.00
Eva Martín2026-09-1533.25
  • JOIN clientes indica la segunda tabla.
  • ON dice cómo se emparejan las filas: el cliente_id del pedido con el id del cliente.
  • Como las dos tablas tienen una columna id, se escribe tabla.columna para 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;
nombrefechaimporte
Ana García2026-09-0145.00
Marta Ruiz2026-09-03120.50
Ana García2026-09-1012.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;
nombrepedidoimporte
Marta Ruiz2120.50
Luis Pérez460.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;
nombreimporte
Ana García45.00
Ana García12.99
Luis Pérez60.00
Marta Ruiz120.50
Pablo DíazNULL
Eva Martín33.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;
nombreimporte
Ana GarcíaNULL
Luis Pérez60.00
Marta Ruiz120.50
Pablo DíazNULL
Eva MartínNULL
-- 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;
nombreimporte
Marta Ruiz120.50
Luis Pérez60.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;
nombrepedidostotal
Marta Ruiz1120.50
Luis Pérez160.00
Ana García257.99
Eva Martín133.25
Pablo Díaz00
  • COUNT(p.id) cuenta solo los pedidos que existen, por eso Pablo tiene 0. Con COUNT(*) contaría 1, por la fila con NULL.
  • COALESCE(valor, 0) devuelve 0 cuando el valor es NULL.
  • Se agrupa por c.id y c.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;
pedidofechaproductocantidadprecio
12026-09-01Mosquetón67.50
22026-09-03Arnés160.00
22026-09-03Casco160.50
32026-09-10Cepillo de presas14.99
32026-09-10Magnesio18.00
42026-09-12Arnés160.00
52026-09-15Bolsa de magnesio117.25
52026-09-15Magnesio28.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_idproducto_idcantidadmotivo
251Talla pequeña
331Llegó 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 del WHERE en 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 tienen id. Escribe c.id o p.id.
  • Emparejar las columnas equivocadas: ON p.id = c.id no 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 JOIN en el WHERE: lo convierte en un INNER JOIN.
  • Usar COUNT(*) tras un LEFT JOIN: cuenta la fila de NULL. Usa COUNT(columna_de_la_derecha).
  • Sumar después de multiplicar filas: si unes pedidos con lineas_pedido, cada pedido aparece una vez por línea. Un SUM(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: como LEFT 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 tablasFROM a JOIN b ON b.a_id = a.id
Todas las filas de la izquierda, con o sin parejaFROM a LEFT JOIN b ON b.a_id = a.id
Las filas de la izquierda sin parejaLEFT JOIN ... WHERE b.id IS NULL
Unir tres o más tablasUn JOIN ... ON por cada tabla nueva
Unir por columnas con el mismo nombreJOIN b USING (columna)
Contar hijos, incluidos los que tienen 0LEFT JOIN + COUNT(b.id) + GROUP BY
  • JOIN a secas es INNER JOIN.
  • Pon alias a las tablas y escribe siempre alias.columna.
  • Con LEFT JOIN, las condiciones sobre la tabla de la derecha van en el ON.

Pon a prueba lo que has aprendido

[SQL] ¿Qué devuelve un INNER JOIN entre clientes y pedidos?

[SQL] Pablo Díaz (cliente 4) no tiene ningún pedido. ¿Qué valor sale en la columna n?
SELECT c.nombre, COUNT(*) AS n
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE c.id = 4
GROUP BY c.id, c.nombre;

[SQL] Hay 5 clientes y solo 2 pedidos superan los 50 €. ¿Cuántas filas devuelve esta consulta?
SELECT c.nombre, p.importe
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE p.importe > 50;

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.