Curso de SQL completo: SELECT, JOIN, agregaciones, índices y transacciones (2026)

A diferencia del INNER JOIN, que solo devuelve filas con correspondencia en ambas tablas, los JOINs externos incluyen también filas que no tienen pareja en la otra tabla, rellenando con NULL las columnas de la tabla que no tiene correspondencia. Son esenciales para responder preguntas como «¿qué clientes no han hecho ningún pedido?» o «¿qué productos nunca se han vendido?».

En este capítulo cubrimos LEFT JOIN, RIGHT JOIN, cómo detectar registros huérfanos y la simulación de FULL OUTER JOIN en MySQL, que no lo soporta de forma nativa.

LEFT JOIN: todas las filas de la tabla izquierda

-- Todos los clientes, tengan o no pedidos
-- Las columnas de pedidos serán NULL para clientes sin pedidos
SELECT
    c.nombre,
    c.email,
    p.id     AS pedido_id,
    p.estado,
    p.total
FROM clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id
ORDER BY c.nombre, p.fecha;

Detectar filas sin correspondencia

-- Clientes que NO han hecho ningún pedido
SELECT c.nombre, c.email
FROM clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id
WHERE p.id IS NULL;

-- Productos que nunca han sido vendidos (no aparecen en lineas_pedido)
SELECT pr.nombre, pr.precio, pr.stock
FROM productos pr
LEFT JOIN lineas_pedido lp ON lp.id_producto = pr.id
WHERE lp.id IS NULL;

El patrón LEFT JOIN ... WHERE tabla_derecha.columna IS NULL es la forma estándar de encontrar registros huérfanos o elementos que no participan en una relación. Es más eficiente que usar NOT IN o NOT EXISTS en la mayoría de los casos.

Contar con LEFT JOIN

-- Número de pedidos por cliente (incluidos los que tienen cero pedidos)
SELECT
    c.nombre,
    COUNT(p.id) AS num_pedidos  -- COUNT(columna) no cuenta NULL
FROM clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id
GROUP BY c.id, c.nombre
ORDER BY num_pedidos DESC;

RIGHT JOIN

RIGHT JOIN es el espejo del LEFT JOIN: incluye todas las filas de la tabla de la derecha aunque no tengan correspondencia en la de la izquierda. En la práctica, casi siempre se puede reescribir como un LEFT JOIN invirtiendo el orden de las tablas, y la mayoría de los desarrolladores prefieren usar siempre LEFT JOIN para mantener consistencia:

-- Estos dos son equivalentes:
SELECT c.nombre, p.id
FROM pedidos p RIGHT JOIN clientes c ON c.id = p.id_cliente;

SELECT c.nombre, p.id
FROM clientes c LEFT JOIN pedidos p ON p.id_cliente = c.id;

Condiciones en ON vs WHERE con LEFT JOIN

-- Diferencia crítica: filtrar en ON vs WHERE con LEFT JOIN

-- Esto devuelve TODOS los clientes; para los de Madrid, muestra pedidos pagados
-- Para el resto, muestra sus pedidos (de cualquier estado) o NULL si no hay pedidos
SELECT c.nombre, p.estado
FROM clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id AND p.estado = 'pagado';

-- Esto devuelve SOLO clientes con al menos un pedido pagado
-- (el LEFT JOIN se convierte efectivamente en INNER JOIN)
SELECT c.nombre, p.estado
FROM clientes c
LEFT JOIN pedidos p ON p.id_cliente = c.id
WHERE p.estado = 'pagado';

FULL OUTER JOIN: todos los registros de ambas tablas

MySQL no soporta FULL OUTER JOIN de forma nativa. La solución es combinar LEFT JOIN y RIGHT JOIN con UNION:

-- Simular FULL OUTER JOIN en MySQL
SELECT c.nombre AS cliente, p.id AS pedido
FROM clientes c LEFT JOIN pedidos p ON p.id_cliente = c.id

UNION

SELECT c.nombre AS cliente, p.id AS pedido
FROM clientes c RIGHT JOIN pedidos p ON p.id_cliente = c.id
WHERE c.id IS NULL;

-- PostgreSQL tiene FULL OUTER JOIN nativo:
SELECT c.nombre, p.id
FROM clientes c FULL OUTER JOIN pedidos p ON p.id_cliente = c.id;

Múltiples LEFT JOINs

-- Todos los clientes con su último pedido y el número de productos que compraron
SELECT
    c.nombre,
    MAX(p.fecha)    AS ultimo_pedido,
    COUNT(DISTINCT lp.id_producto) AS productos_distintos
FROM clientes c
LEFT JOIN pedidos      p  ON  p.id_cliente = c.id
LEFT JOIN lineas_pedido lp ON lp.id_pedido  = p.id
GROUP BY c.id, c.nombre
ORDER BY ultimo_pedido DESC NULLS LAST;

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP