En las bases de datos relacionales, la información raramente vive en una sola tabla. Los clientes están en una tabla, sus pedidos en otra, los productos en otra y los vendedores en otra más. El arte de combinar toda esa información en una sola consulta se llama JOIN.
Es uno de los conceptos más importantes de SQL y uno de los más temidos por los principiantes. Pero una vez que lo entiendes con la analogía correcta, todo encaja.
En este artículo vas a aprender todos los tipos de JOIN, cuándo usar cada uno y cómo escribirlos correctamente, con el mismo conjunto de datos en todos los ejemplos para que puedas ver claramente las diferencias.
¿Qué es un JOIN y para qué sirve?
Un JOIN es una cláusula de SQL que combina filas de dos o más tablas basándose en una columna que tienen en común, generalmente una clave primaria y una clave foránea.
La analogía perfecta: imagina dos listas en papel. Una lista tiene los nombres de estudiantes con su número de ID. La otra lista tiene las notas de los exámenes con el número de ID del estudiante que las obtuvo. Para saber quién sacó cada nota, necesitas comparar ambas listas y unir la información por el número de ID. Eso es exactamente lo que hace un JOIN.
Las tablas de ejemplo — úsalas en todos los ejercicios
Vamos a usar estas dos tablas durante todo el artículo. Créalas en tu gestor de base de datos favorito para seguir los ejemplos:
-- Tabla de clientes
CREATE TABLE clientes (
id INT PRIMARY KEY,
nombre VARCHAR(50),
ciudad VARCHAR(50)
);
INSERT INTO clientes VALUES (1, 'Ana García', 'Madrid');
INSERT INTO clientes VALUES (2, 'Luis Pérez', 'Barcelona');
INSERT INTO clientes VALUES (3, 'María López', 'Sevilla');
INSERT INTO clientes VALUES (4, 'Carlos Ruiz', 'Valencia'); -- sin pedidos
-- Tabla de pedidos
CREATE TABLE pedidos (
id INT PRIMARY KEY,
cliente_id INT,
producto VARCHAR(50),
total DECIMAL(10,2)
);
INSERT INTO pedidos VALUES (101, 1, 'Laptop', 899.99);
INSERT INTO pedidos VALUES (102, 1, 'Mouse', 29.99);
INSERT INTO pedidos VALUES (103, 2, 'Teclado', 79.99);
INSERT INTO pedidos VALUES (104, 3, 'Monitor', 399.99);
INSERT INTO pedidos VALUES (105, 9, 'Auriculares', 49.99); -- cliente_id 9 no existeFíjate en dos detalles importantes que generarán diferencias entre los tipos de JOIN:
- 🔴 Carlos Ruiz (id=4) no tiene ningún pedido
- 🔴 El pedido 105 tiene cliente_id=9 que no existe en la tabla clientes
Estos casos "conflictivos" son exactamente los que demuestran para qué sirve cada tipo de JOIN.
La sintaxis base de un JOIN
SELECT columnas
FROM tabla_izquierda
[TIPO DE] JOIN tabla_derecha ON tabla_izquierda.columna = tabla_derecha.columna
[WHERE condicion]
[ORDER BY columna];Tres partes clave:
- FROM tabla_izquierda — la tabla principal de la consulta
- [TIPO] JOIN tabla_derecha — la tabla con la que se combina
- ON condición — la condición que relaciona las dos tablas (casi siempre una igualdad entre claves)
INNER JOIN — solo las coincidencias
El INNER JOIN es el más usado. Devuelve únicamente las filas que tienen coincidencia en ambas tablas. Si una fila no tiene pareja en la otra tabla, queda fuera del resultado.
-- Obtener clientes con sus pedidos (solo los que tienen pedidos)
SELECT
c.nombre AS cliente,
c.ciudad,
p.producto,
p.total
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id
ORDER BY c.nombre;Resultado:
┌──────────────┬───────────┬──────────┬────────┐
│ cliente │ ciudad │ producto │ total │
├──────────────┼───────────┼──────────┼────────┤
│ Ana García │ Madrid │ Laptop │ 899.99 │
│ Ana García │ Madrid │ Mouse │ 29.99 │
│ Luis Pérez │ Barcelona │ Teclado │ 79.99 │
│ María López │ Sevilla │ Monitor │ 399.99 │
└──────────────┴───────────┴──────────┴────────┘Observa que:
- ❌ Carlos Ruiz no aparece — no tiene pedidos
- ❌ El pedido 105 (Auriculares) no aparece — su cliente_id=9 no existe
- ✅ Ana García aparece dos veces — tiene dos pedidos
💡 Dato útil: escribir solo JOIN sin especificar tipo es lo mismo que INNER JOIN. Es el JOIN por defecto.
-- Estas dos consultas son idénticas:
FROM clientes c INNER JOIN pedidos p ON c.id = p.cliente_id
FROM clientes c JOIN pedidos p ON c.id = p.cliente_id¿Cuándo usar INNER JOIN?
Cuando solo te interesan los registros que tienen relación en ambas tablas. Es el caso más común: pedidos con su cliente, empleados con su departamento, productos con su categoría.
LEFT JOIN — todos de la izquierda, coincidencias de la derecha
El LEFT JOIN (o LEFT OUTER JOIN) devuelve todas las filas de la tabla izquierda (la que está en el FROM), más las coincidencias de la tabla derecha. Si no hay coincidencia en la tabla derecha, los campos de esa tabla aparecen como NULL.
-- Todos los clientes, tengan o no pedidos
SELECT
c.nombre AS cliente,
c.ciudad,
p.producto,
p.total
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
ORDER BY c.nombre;Resultado:
┌──────────────┬───────────┬──────────┬────────┐
│ cliente │ ciudad │ producto │ total │
├──────────────┼───────────┼──────────┼────────┤
│ Ana García │ Madrid │ Laptop │ 899.99 │
│ Ana García │ Madrid │ Mouse │ 29.99 │
│ Carlos Ruiz │ Valencia │ NULL │ NULL │ ← sin pedidos
│ Luis Pérez │ Barcelona │ Teclado │ 79.99 │
│ María López │ Sevilla │ Monitor │ 399.99 │
└──────────────┴───────────┴──────────┴────────┘Ahora Carlos Ruiz aparece aunque no tenga pedidos, con NULL en las columnas de pedidos. El pedido 105 (Auriculares) sigue sin aparecer porque viene de la tabla derecha.
Truco: encontrar registros sin coincidencia
El LEFT JOIN tiene un uso muy poderoso: encontrar filas que no tienen pareja en la otra tabla, filtrando por NULL:
-- Clientes que NUNCA han hecho un pedido
SELECT c.nombre, c.ciudad
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
WHERE p.id IS NULL;┌─────────────┬──────────┐
│ nombre │ ciudad │
├─────────────┼──────────┤
│ Carlos Ruiz │ Valencia │
└─────────────┴──────────┘¿Cuándo usar LEFT JOIN?
Cuando necesitas todos los registros de la tabla principal aunque no tengan datos relacionados en la otra tabla. Casos clásicos: listar todos los usuarios aunque no hayan comprado, todos los empleados aunque no estén asignados a un proyecto, todos los productos aunque no tengan stock.
RIGHT JOIN — todos de la derecha, coincidencias de la izquierda
El RIGHT JOIN es exactamente el opuesto al LEFT JOIN. Devuelve todas las filas de la tabla derecha (la que está en el JOIN), más las coincidencias de la tabla izquierda.
-- Todos los pedidos, tengan o no cliente registrado
SELECT
c.nombre AS cliente,
p.producto,
p.total
FROM clientes c
RIGHT JOIN pedidos p ON c.id = p.cliente_id
ORDER BY p.id;Resultado:
┌─────────────┬──────────────┬────────┐
│ cliente │ producto │ total │
├─────────────┼──────────────┼────────┤
│ Ana García │ Laptop │ 899.99 │
│ Ana García │ Mouse │ 29.99 │
│ Luis Pérez │ Teclado │ 79.99 │
│ María López │ Monitor │ 399.99 │
│ NULL │ Auriculares │ 49.99 │ ← cliente no existe
└─────────────┴──────────────┴────────┘Ahora aparece el pedido 105 (Auriculares) con NULL en el nombre del cliente porque su cliente_id no existe. Carlos Ruiz no aparece porque viene de la tabla izquierda.
💡 Consejo profesional: En la práctica, el RIGHT JOIN casi nunca se ve en código de producción. Siempre puedes reescribir un RIGHT JOIN como LEFT JOIN simplemente intercambiando el orden de las tablas. Los LEFT JOINs son más intuitivos de leer.
-- Estas dos consultas dan exactamente el mismo resultado:
FROM clientes c RIGHT JOIN pedidos p ON c.id = p.cliente_id
FROM pedidos p LEFT JOIN clientes c ON c.id = p.cliente_id¿Cuándo usar RIGHT JOIN?
Cuando necesitas todos los registros de la tabla secundaria aunque no tengan cliente/padre en la tabla principal. En la práctica, casi siempre es más claro reescribirlo como LEFT JOIN cambiando el orden de las tablas.
FULL JOIN — todos de ambas tablas
El FULL JOIN (o FULL OUTER JOIN) devuelve todas las filas de ambas tablas, haya o no coincidencia. Donde no hay coincidencia, aparece NULL en las columnas de la tabla que no tiene pareja.
-- PostgreSQL / SQL Server: todos los clientes y todos los pedidos
SELECT
c.nombre AS cliente,
p.producto,
p.total
FROM clientes c
FULL JOIN pedidos p ON c.id = p.cliente_id
ORDER BY c.nombre, p.id;Resultado:
┌─────────────┬──────────────┬────────┐
│ cliente │ producto │ total │
├─────────────┼──────────────┼────────┤
│ Ana García │ Laptop │ 899.99 │
│ Ana García │ Mouse │ 29.99 │
│ Carlos Ruiz │ NULL │ NULL │ ← cliente sin pedidos
│ Luis Pérez │ Teclado │ 79.99 │
│ María López │ Monitor │ 399.99 │
│ NULL │ Auriculares │ 49.99 │ ← pedido sin cliente
└─────────────┴──────────────┴────────┘Ahora aparecen todos: Carlos Ruiz sin pedidos y el pedido de Auriculares sin cliente.
FULL JOIN en MySQL (no tiene soporte nativo)
MySQL no soporta FULL JOIN directamente. Se simula combinando LEFT JOIN y RIGHT JOIN con UNION:
-- Simulación de FULL JOIN en MySQL
SELECT c.nombre, p.producto, p.total
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
UNION
SELECT c.nombre, p.producto, p.total
FROM clientes c
RIGHT JOIN pedidos p ON c.id = p.cliente_id;¿Cuándo usar FULL JOIN?
Para auditorías y comparaciones totales donde necesitas ver todo sin perder ningún registro de ninguna tabla. Por ejemplo: comparar dos sistemas de datos para encontrar discrepancias, auditar qué facturas no tienen pago y qué pagos no tienen factura.
CROSS JOIN — todas las combinaciones posibles
El CROSS JOIN produce el producto cartesiano de dos tablas: combina cada fila de la primera tabla con cada fila de la segunda. No necesita condición ON.
-- Todas las combinaciones posibles de clientes y productos
SELECT
c.nombre AS cliente,
p.producto AS posible_compra
FROM clientes c
CROSS JOIN pedidos p
ORDER BY c.nombre;Si clientes tiene 4 filas y pedidos tiene 5 filas: resultado = 4 × 5 = 20 filas.
-- Ejemplo más útil: generar un calendario
-- Todos los empleados × todos los meses del año
SELECT
e.nombre AS empleado,
m.mes
FROM empleados e
CROSS JOIN (
SELECT 1 AS mes UNION SELECT 2 UNION SELECT 3 UNION
SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION
SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION
SELECT 10 UNION SELECT 11 UNION SELECT 12
) m
ORDER BY e.nombre, m.mes;⚠️ Cuidado con el CROSS JOIN
Si cruzas tablas grandes, el resultado puede ser masivo. Dos tablas de 1,000 filas cada una generan 1,000,000 de filas. Siempre verifica el tamaño de las tablas antes de usar CROSS JOIN sin filtros.
¿Cuándo usar CROSS JOIN?
Para generar combinaciones: horarios (empleados × turnos), calendarios (personas × meses), menús (platos × bebidas), escenarios de prueba. Es útil pero poco frecuente en consultas de producción.
SELF JOIN — una tabla unida consigo misma
El SELF JOIN no es un tipo de JOIN diferente en sintaxis, sino una técnica: unir una tabla consigo misma usando alias. Se usa cuando una tabla tiene una relación jerárquica interna.
-- Tabla de empleados con referencia al jefe (que también es empleado)
CREATE TABLE empleados (
id INT PRIMARY KEY,
nombre VARCHAR(50),
jefe_id INT -- referencia al id del jefe (NULL si es el director)
);
INSERT INTO empleados VALUES (1, 'Director García', NULL);
INSERT INTO empleados VALUES (2, 'Ana Martínez', 1);
INSERT INTO empleados VALUES (3, 'Luis Torres', 1);
INSERT INTO empleados VALUES (4, 'Carlos Soto', 2);
INSERT INTO empleados VALUES (5, 'María Vega', 2);-- Obtener cada empleado junto con el nombre de su jefe
SELECT
e.nombre AS empleado,
j.nombre AS jefe
FROM empleados e
LEFT JOIN empleados j ON e.jefe_id = j.id
ORDER BY j.nombre, e.nombre;┌─────────────────┬──────────────────┐
│ empleado │ jefe │
├─────────────────┼──────────────────┤
│ Director García │ NULL │ ← no tiene jefe
│ Ana Martínez │ Director García │
│ Luis Torres │ Director García │
│ Carlos Soto │ Ana Martínez │
│ María Vega │ Ana Martínez │
└─────────────────┴──────────────────┘¿Cuándo usar SELF JOIN?
Para jerarquías y relaciones dentro de la misma tabla: organigramas (empleado-jefe), categorías con subcategorías, comentarios con respuestas, rutas de navegación.
JOIN con múltiples tablas
En proyectos reales, raramente combinas solo dos tablas. Puedes encadenar tantos JOINs como necesites:
-- Consulta completa: cliente → pedido → producto → categoría
SELECT
c.nombre AS cliente,
p.id AS pedido_id,
pr.nombre AS producto,
cat.nombre AS categoria,
p.total
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id
INNER JOIN productos pr ON p.prod_id = pr.id
INNER JOIN categorias cat ON pr.cat_id = cat.id
WHERE p.total > 100
ORDER BY p.total DESC;Puedes mezclar tipos de JOIN en la misma consulta:
-- Todos los empleados con su departamento (si tienen) y sus proyectos (si tienen)
SELECT
e.nombre AS empleado,
d.nombre AS departamento,
p.nombre AS proyecto
FROM empleados e
LEFT JOIN departamentos d ON e.dept_id = d.id -- puede no tener departamento
LEFT JOIN proyectos p ON e.id = p.emp_id -- puede no tener proyecto
ORDER BY e.nombre;Buenas prácticas con JOINs
✅ Siempre usa alias para las tablas
-- ❌ Difícil de leer con nombres completos
SELECT clientes.nombre, pedidos.total
FROM clientes
INNER JOIN pedidos ON clientes.id = pedidos.cliente_id;
-- ✅ Mucho más limpio con alias
SELECT c.nombre, p.total
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id;✅ Selecciona solo las columnas que necesitas
-- ❌ Evita SELECT * en JOINs — puede traer columnas duplicadas
SELECT * FROM clientes c INNER JOIN pedidos p ON c.id = p.cliente_id;
-- ✅ Especifica exactamente qué columnas quieres
SELECT c.nombre, c.ciudad, p.producto, p.total
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id;✅ Usa índices en las columnas de JOIN
-- Las columnas usadas en la condición ON deben tener índices
-- para que el JOIN sea eficiente en tablas grandes
CREATE INDEX idx_pedidos_cliente_id ON pedidos(cliente_id);✅ Filtra con WHERE o AND en el ON según el caso
-- Filtrar en WHERE: elimina filas del resultado final
SELECT c.nombre, p.producto
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
WHERE p.total > 100; -- ← elimina clientes sin pedidos también
-- Filtrar en ON: solo afecta al JOIN, mantiene todos los clientes
SELECT c.nombre, p.producto
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id AND p.total > 100;
-- ← mantiene todos los clientes, pero solo trae pedidos > 100Tabla resumen: todos los tipos de JOIN
| Tipo | ¿Qué devuelve? | ¿Cuándo usar? | MySQL | PostgreSQL |
|---|---|---|---|---|
| INNER JOIN | Solo filas con coincidencia en AMBAS tablas | El caso más común — solo datos relacionados | ✅ | ✅ |
| LEFT JOIN | Todas de la izquierda + coincidencias de la derecha | Todos los registros principales aunque no tengan relacionados | ✅ | ✅ |
| RIGHT JOIN | Todas de la derecha + coincidencias de la izquierda | Igual que LEFT pero priorizando la tabla secundaria | ✅ | ✅ |
| FULL JOIN | Todas de AMBAS tablas, haya o no coincidencia | Auditorías, comparaciones totales sin perder nada | ❌ (simular con UNION) | ✅ |
| CROSS JOIN | Producto cartesiano — todas las combinaciones posibles | Generar combinaciones: horarios, menús, escenarios | ✅ | ✅ |
| SELF JOIN | Una tabla unida consigo misma (con alias) | Jerarquías internas: jefe-empleado, categorías | ✅ | ✅ |
Errores comunes con JOINs
Confundir LEFT JOIN con INNER JOIN
-- ❌ Quieres todos los clientes pero usas INNER JOIN
SELECT c.nombre, p.total
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id;
-- Resultado: Carlos Ruiz no aparece
-- ✅ Para todos los clientes, usa LEFT JOIN
SELECT c.nombre, p.total
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id;Olvidar la condición ON — genera un CROSS JOIN accidental
-- ❌ Sin condición ON → producto cartesiano no deseado
SELECT c.nombre, p.producto
FROM clientes c, pedidos p; -- sintaxis antigua sin ON
-- Resultado: 4 clientes × 5 pedidos = 20 filas incorrectas
-- ✅ Siempre especifica la condición de unión
SELECT c.nombre, p.producto
FROM clientes c
INNER JOIN pedidos p ON c.id = p.cliente_id;Duplicación inesperada de filas
-- Si un cliente tiene 3 pedidos, aparece 3 veces en el JOIN
-- Esto es correcto en un INNER JOIN
-- Si solo quieres un registro por cliente, agrupa:
SELECT c.nombre, COUNT(p.id) AS total_pedidos, SUM(p.total) AS gasto_total
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id, c.nombre
ORDER BY gasto_total DESC;Filtrar con WHERE en vez de ON en un LEFT JOIN
-- ❌ Esto convierte el LEFT JOIN en un INNER JOIN de facto
SELECT c.nombre, p.producto
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
WHERE p.total > 100; -- elimina los clientes con p.total = NULL
-- ✅ Si quieres filtrar pero mantener todos los clientes:
SELECT c.nombre, p.producto
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id AND p.total > 100;Resumen: lo que aprendiste hoy
- ✅ Un JOIN combina filas de dos tablas usando una columna en común (condición ON)
- ✅ INNER JOIN — solo las filas con coincidencia en ambas tablas (el más común)
- ✅ LEFT JOIN — todas las filas de la izquierda + coincidencias de la derecha (NULL si no hay)
- ✅ RIGHT JOIN — todas las filas de la derecha + coincidencias de la izquierda
- ✅ FULL JOIN — todas las filas de ambas tablas (MySQL lo simula con UNION)
- ✅ CROSS JOIN — producto cartesiano de todas las combinaciones posibles
- ✅ SELF JOIN — una tabla unida consigo misma para relaciones jerárquicas
- ✅ En el 99% de los casos de producción, solo necesitas INNER JOIN y LEFT JOIN
- ✅ Siempre usa alias, selecciona columnas específicas e indexa las columnas del ON
- ✅ La diferencia entre filtrar en WHERE vs ON puede cambiar completamente el resultado
🧪 ¿Tienes bien sólidas tus bases de SQL?
Los JOINs son SQL intermedio. Para dominarlos de verdad necesitas tener bien claros los fundamentos: SELECT, WHERE, GROUP BY, ORDER BY y las funciones de agregación. Comprueba dónde estás:
👉 Test: SQL Básico 👉 Test: SQL vs NoSQL
¿Cuál tipo de JOIN te costó más entender? ¿Ya usabas JOINs o era la primera vez que los veías? ¿Tienes alguna duda con alguno de los ejemplos? Cuéntanos en los comentarios 👇 — respondemos todos. 🚀
No hay comentarios todavía. Sé el primero en compartir tu opinión.