JOIN en SQL: INNER JOIN y LEFT JOIN con ejemplos y ejercicios

Actualizado el 29 de septiembre de 2026

En una base de datos relacional la información se reparte en varias tablas para no repetirla: los datos de cada cliente se guardan una sola vez, y cada pedido solo guarda el id del cliente que lo hizo. Un JOIN vuelve a juntar esas tablas en una consulta, siguiendo esa relación.

En esta guía se usan dos tablas pequeñas, clientes y pedidos, para que puedas seguir cada resultado fila por fila:

Estructura
CREATE TABLE clientes (
  id     INTEGER PRIMARY KEY,
  nombre TEXT,
  ciudad TEXT
);

CREATE TABLE pedidos (
  id         INTEGER PRIMARY KEY,
  cliente_id INTEGER REFERENCES clientes(id),
  monto      INTEGER
);
clientes
idnombreciudad
1AnaSantiago
2BrunoValparaíso
3CarlaConcepción
4DiegoSantiago
pedidos
idcliente_idmonto
101115000
10218000
103322000
10445000

Fíjate en que Ana tiene dos pedidos y Bruno ninguno. Esos dos casos son los que marcan la diferencia entre los distintos tipos de JOIN.

INNER JOIN: solo las filas que tienen pareja

INNER JOIN combina cada fila de una tabla con las filas de la otra que cumplen la condición de ON. Las filas sin pareja quedan fuera del resultado.

SELECT c.nombre, p.id AS pedido, p.monto
FROM clientes c
INNER JOIN pedidos p ON p.cliente_id = c.id
ORDER BY p.id;
nombrepedidomonto
Ana10115000
Ana1028000
Carla10322000
Diego1045000

Ana aparece dos veces, una por cada pedido, y Bruno no aparece porque no tiene pedidos. Las letras c y p son alias: nombres cortos para las tablas que evitan escribir clientes.nombre cada vez. Escribir solo JOIN es lo mismo que INNER JOIN.

LEFT JOIN: todas las filas de la tabla de la izquierda

LEFT JOIN conserva todas las filas de la tabla que va antes del JOIN (la de la izquierda), tengan o no pareja. Cuando no hay pareja, las columnas de la otra tabla vienen en NULL.

SELECT c.nombre, p.id AS pedido, p.monto
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
ORDER BY c.id, p.id;
nombrepedidomonto
Ana10115000
Ana1028000
BrunoNULLNULL
Carla10322000
Diego1045000

Ahora Bruno sí aparece, con NULL en las columnas del pedido. Usa LEFT JOIN cuando la pregunta es sobre todos los elementos de una tabla, incluidos los que no tienen nada relacionado: todos los clientes, todos los productos, todos los alumnos.

También existen RIGHT JOIN (conserva la tabla de la derecha) y FULL OUTER JOIN (conserva ambas). En la práctica casi siempre se escribe LEFT JOIN y se ordenan las tablas para que la importante quede a la izquierda.

JOIN con GROUP BY: totales por cliente

Un uso muy común es juntar las tablas y después agrupar. Por ejemplo, cuántos pedidos hizo cada cliente y cuánto gastó en total, incluyendo a los que no han comprado:

SELECT c.nombre,
       COUNT(p.id)              AS pedidos,
       COALESCE(SUM(p.monto), 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
Ana223000
Carla122000
Diego15000
Bruno00

COUNT(p.id) cuenta solo los valores que no son NULL, así que Bruno queda con 0. SUM de puros NULL devuelve NULL, y COALESCE lo reemplaza por 0. Se agrupa por c.id además del nombre para no mezclar a dos clientes que se llamen igual.

Errores comunes

Contar con COUNT(*) después de un LEFT JOIN

Si en la consulta anterior usas COUNT(*), Bruno aparece con 1 pedido: COUNT(*) cuenta filas, y el LEFT JOIN generó una fila para Bruno (con el pedido en NULL). Para contar elementos relacionados, cuenta una columna de la tabla de la derecha, como COUNT(p.id).

Filtrar la tabla de la derecha en el WHERE

-- Intención: todos los clientes, con sus pedidos de más de 10.000
SELECT c.nombre, p.monto
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE p.monto > 10000;
nombremonto
Ana15000
Carla22000

Bruno y Diego desaparecieron: el WHERE se aplica después del JOIN, y NULL > 10000 no es verdadero, así que descarta las filas sin pareja. En la práctica, el LEFT JOIN pasó a comportarse como un INNER JOIN. Si la condición es parte de qué filas emparejar, va en el ON:

SELECT c.nombre, p.monto
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id AND p.monto > 10000
ORDER BY c.id;
nombremonto
Ana15000
BrunoNULL
Carla22000
DiegoNULL

Olvidar la condición de unión

SELECT * FROM clientes, pedidos (o un JOIN con una condición mal escrita) combina cada cliente con cada pedido: 4 × 4 = 16 filas sin sentido. Si el resultado tiene muchas más filas de las que esperabas, revisa el ON.

Columnas ambiguas

Las dos tablas tienen una columna id, así que SELECT id falla con un error de columna ambigua. Indica siempre de qué tabla viene cada columna (c.id, p.id) cuando hay más de una tabla en la consulta.

Ejercicios resueltos

Intenta resolver cada ejercicio antes de abrir la solución: equivocarse y corregir es la parte que más enseña.

1.Pedidos con el nombre del cliente

Con las mismas tablas, escribe una consulta que muestre el nombre del cliente y el monto de cada pedido, ordenados del monto más alto al más bajo.

Ver solución
Solución
SELECT c.nombre, p.monto
FROM pedidos p
INNER JOIN clientes c ON c.id = p.cliente_id
ORDER BY p.monto DESC;
nombremonto
Carla22000
Ana15000
Ana8000
Diego5000

La pregunta es sobre pedidos, y todos los pedidos tienen un cliente, así que basta un INNER JOIN. El orden de las tablas no cambia el resultado de un INNER JOIN: partir desde pedidos solo hace que la consulta se lea igual que la pregunta.

2.Clientes que nunca han comprado

Escribe una consulta que devuelva el nombre y la ciudad de los clientes que no tienen ningún pedido.

Ver solución
Solución
SELECT c.nombre, c.ciudad
FROM clientes c
LEFT JOIN pedidos p ON p.cliente_id = c.id
WHERE p.id IS NULL;
nombreciudad
BrunoValparaíso

El LEFT JOIN conserva a todos los clientes y deja en NULL las columnas del pedido cuando no hay pareja. Después, WHERE p.id IS NULL se queda justo con esas filas. Es un patrón muy usado para encontrar lo que falta: productos sin ventas, alumnos sin notas, usuarios sin actividad.

Ojo: para comparar con NULL se usa IS NULL, nunca = NULL, que no devuelve ninguna fila. Otra forma de escribir lo mismo es con WHERE NOT EXISTS (SELECT 1 FROM pedidos p WHERE p.cliente_id = c.id).

3.Total vendido por ciudad

Escribe una consulta que muestre, para cada ciudad con al menos un pedido, cuántos pedidos se hicieron y el monto total, de la ciudad que más vendió a la que menos.

Ver solución
Solución
SELECT c.ciudad,
       COUNT(p.id)  AS pedidos,
       SUM(p.monto) AS total
FROM clientes c
INNER JOIN pedidos p ON p.cliente_id = c.id
GROUP BY c.ciudad
ORDER BY total DESC;
ciudadpedidostotal
Santiago328000
Concepción122000

La ciudad está en clientes y los montos en pedidos, así que primero hay que juntarlas. Como solo interesan las ciudades con pedidos, sirve un INNER JOIN: Valparaíso queda fuera sola, porque Bruno no tiene pedidos. Después se agrupa por ciudad: Santiago suma los dos pedidos de Ana y el de Diego.

Practica SQL con ejercicios a tu nivel

En Coditer resuelves ejercicios en el navegador, con tests automáticos que revisan tu código y una IA que te da pistas cuando te atascas. Practicar es gratis.