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:
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
);| id | nombre | ciudad |
|---|---|---|
| 1 | Ana | Santiago |
| 2 | Bruno | Valparaíso |
| 3 | Carla | Concepción |
| 4 | Diego | Santiago |
| id | cliente_id | monto |
|---|---|---|
| 101 | 1 | 15000 |
| 102 | 1 | 8000 |
| 103 | 3 | 22000 |
| 104 | 4 | 5000 |
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;| nombre | pedido | monto |
|---|---|---|
| Ana | 101 | 15000 |
| Ana | 102 | 8000 |
| Carla | 103 | 22000 |
| Diego | 104 | 5000 |
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;| nombre | pedido | monto |
|---|---|---|
| Ana | 101 | 15000 |
| Ana | 102 | 8000 |
| Bruno | NULL | NULL |
| Carla | 103 | 22000 |
| Diego | 104 | 5000 |
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;| nombre | pedidos | total |
|---|---|---|
| Ana | 2 | 23000 |
| Carla | 1 | 22000 |
| Diego | 1 | 5000 |
| Bruno | 0 | 0 |
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;| nombre | monto |
|---|---|
| Ana | 15000 |
| Carla | 22000 |
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;| nombre | monto |
|---|---|
| Ana | 15000 |
| Bruno | NULL |
| Carla | 22000 |
| Diego | NULL |
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ónOcultar 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;| nombre | monto |
|---|---|
| Carla | 22000 |
| Ana | 15000 |
| Ana | 8000 |
| Diego | 5000 |
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ónOcultar 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;| nombre | ciudad |
|---|---|
| Bruno | Valparaí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ónOcultar 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;| ciudad | pedidos | total |
|---|---|---|
| Santiago | 3 | 28000 |
| Concepción | 1 | 22000 |
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.