LEFT JOIN
El LEFT JOIN (también llamado LEFT OUTER JOIN) devuelve todas las filas de la tabla izquierda, tengan o no correspondencia en la tabla derecha. Cuando no hay coincidencia, las columnas de la tabla derecha se rellenan con NULL. Esto lo diferencia del INNER JOIN, que descarta las filas sin correspondencia.
El LEFT JOIN es esencial cuando necesitas preservar todos los registros de una tabla principal y enriquecerlos con información de otra tabla que puede no existir para todos ellos.
Sintaxis
SELECT columnas
FROM tabla_izquierda
LEFT JOIN tabla_derecha ON tabla_izquierda.columna = tabla_derecha.columna;La tabla que aparece después de FROM es la tabla izquierda (la que se preserva completa). La tabla que aparece después de LEFT JOIN es la derecha (la que puede aportar NULLs cuando no hay coincidencia).
Cómo funciona
El proceso es similar al INNER JOIN, pero con una diferencia crucial en el paso final:
- MySQL toma cada fila de la tabla izquierda.
- Para cada una, busca en la tabla derecha las filas que cumplan la condición del
ON. - Si encuentra coincidencias, combina las columnas de ambas filas, igual que un INNER JOIN.
- Si no encuentra ninguna coincidencia, la fila de la tabla izquierda se mantiene en el resultado, y todas las columnas de la tabla derecha se llenan con
NULL.
El resultado siempre contiene al menos tantas filas como la tabla izquierda. Si una fila de la tabla izquierda tiene varias coincidencias en la derecha, aparece repetida (una vez por cada coincidencia).
LEFT JOIN básico
Todos los clientes con el número de pedidos que han realizado, incluyendo aquellos que no han comprado nada:
SELECT
cl.nombre,
cl.apellidos,
COUNT(p.id) AS total_pedidos
FROM clientes cl
LEFT JOIN pedidos p ON cl.id = p.cliente_id
GROUP BY cl.id, cl.nombre, cl.apellidos
ORDER BY total_pedidos, cl.nombre;| nombre | apellidos | total_pedidos |
|---|---|---|
| Diego | Castro Iglesias | 0 |
| Alejandro | Serrano Blanco | 1 |
| Andrés | Vega Romero | 1 |
| Carmen | Ruiz Jiménez | 1 |
| Elena | Romero Navarro | 1 |
| Fernando | Molina Ortega | 1 |
| Isabel | Gil Molina | 1 |
| Javier | Moreno Díaz | 1 |
| Laura | López Sánchez | 1 |
| Lucía | Díaz Hernández | 1 |
| Marta | Blanco Castro | 1 |
| Miguel | Hernández Torres | 1 |
| Paula | Ortega Serrano | 1 |
| Roberto | Navarro Gil | 1 |
| Sara | Torres Vega | 1 |
| Ana | Martínez Ruiz | 2 |
| Carlos | Rodríguez Martín | 2 |
| David | Sánchez Moreno | 2 |
| Pedro | Fernández Castro | 2 |
| María | García López | 3 |
Diego Castro aparece con 0 pedidos. Con un INNER JOIN, esta fila simplemente no existiría en el resultado. El LEFT JOIN garantiza que los 20 clientes aparezcan, tengan o no pedidos.
Fíjate que usamos COUNT(p.id) y no COUNT(*). Es una distinción importante: COUNT(*) contaría la fila de Diego como 1 (porque la fila existe en el resultado), mientras que COUNT(p.id) devuelve 0 porque p.id es NULL cuando no hay pedido.
Encontrar registros sin correspondencia
Uno de los usos más potentes del LEFT JOIN es encontrar registros que no tienen relación en otra tabla. El patrón es hacer un LEFT JOIN y filtrar con WHERE ... IS NULL:
SELECT cl.nombre, cl.apellidos, cl.email
FROM clientes cl
LEFT JOIN pedidos p ON cl.id = p.cliente_id
WHERE p.id IS NULL;| nombre | apellidos | |
|---|---|---|
| Diego | Castro Iglesias | diego.castro@email.com |
Diego es el único cliente que nunca ha hecho un pedido. Este patrón es extremadamente útil: el LEFT JOIN incluye a todos los clientes, y el WHERE p.id IS NULL filtra para quedarse solo con aquellos donde no hubo coincidencia en la tabla de pedidos.
Productos sin reseñas
El mismo patrón para encontrar productos que nadie ha reseñado:
SELECT p.nombre, p.precio
FROM productos p
LEFT JOIN resenas r ON p.id = r.producto_id
WHERE r.id IS NULL
ORDER BY p.precio DESC;| nombre | precio |
|---|---|
| Lenovo ThinkPad X1 | 1549.00 |
| Sofá 3 plazas | 599.00 |
| Estantería modular | 149.99 |
| Zapatillas trail | 149.99 |
| Vaqueros slim fit | 59.99 |
| Funda iPhone silicona | 49.99 |
| Pantalón chino | 49.99 |
| Sartén antiadherente 28cm | 39.99 |
| Camiseta técnica running | 34.99 |
| Eloquent JavaScript | 34.99 |
| Diseño de APIs | 29.99 |
| Cable USB-C a Lightning | 19.99 |
| Banda elástica set x5 | 19.99 |
| El nombre del viento | 16.99 |
14 de nuestros 30 productos no tienen ninguna reseña. El Lenovo ThinkPad, con un precio de 1549 euros, es el producto más caro sin valoraciones. Esta información sería útil para un equipo de marketing que quiera incentivar reseñas en productos de alto valor.
Productos que nunca se han vendido
Podemos buscar productos que no aparecen en ningún pedido:
SELECT p.nombre, p.precio, p.stock
FROM productos p
LEFT JOIN detalle_pedidos dp ON p.id = dp.producto_id
WHERE dp.id IS NULL
ORDER BY p.nombre;| nombre | precio | stock |
|---|---|---|
| Cargador USB-C 65W | 35.99 | 150 |
| Estantería modular | 149.99 | 22 |
| Sofá 3 plazas | 599.00 | 8 |
Solo 3 productos no se han vendido nunca. El sofá de 3 plazas tiene apenas 8 unidades en stock y ninguna venta, lo que podría indicar un problema de visibilidad o de precio.
Categorías sin productos directos
Las 5 categorías principales de nuestra base de datos (Electrónica, Ropa, Hogar, Deportes, Libros) son categorías padre: no tienen productos asignados directamente, ya que los productos pertenecen a sus subcategorías:
SELECT c.nombre AS categoria, c.descripcion
FROM categorias c
LEFT JOIN productos p ON c.id = p.categoria_id
WHERE p.id IS NULL
ORDER BY c.nombre;| categoria | descripcion |
|---|---|
| Deportes | Equipamiento deportivo y fitness |
| Electrónica | Dispositivos electrónicos y accesorios |
| Hogar | Artículos para el hogar y decoración |
| Libros | Libros físicos y digitales |
| Ropa | Ropa y complementos para hombre y mujer |
Estas categorías actúan como agrupadores de nivel superior. Sus subcategorías (Smartphones, Portátiles, Camisetas, etc.) son las que contienen los productos reales.
LEFT JOIN con agregación
Al combinar LEFT JOIN con funciones de agregación, recuerda usar COALESCE para convertir los NULL en valores legibles:
SELECT
cl.nombre,
cl.apellidos,
COUNT(p.id) AS total_pedidos,
COALESCE(SUM(p.total), 0) AS gasto_total
FROM clientes cl
LEFT JOIN pedidos p ON cl.id = p.cliente_id
GROUP BY cl.id, cl.nombre, cl.apellidos
ORDER BY gasto_total DESC
LIMIT 5;| nombre | apellidos | total_pedidos | gasto_total |
|---|---|---|---|
| Pedro | Fernández Castro | 2 | 1979.97 |
| María | García López | 3 | 1849.95 |
| Sara | Torres Vega | 1 | 1549.00 |
| Carmen | Ruiz Jiménez | 1 | 1399.00 |
| Carlos | Rodríguez Martín | 2 | 1029.98 |
Sin COALESCE, el gasto total de Diego (el cliente sin pedidos) aparecería como NULL en lugar de 0. COALESCE(SUM(p.total), 0) sustituye el NULL por 0 para que la columna sea consistente.
Pedro lidera en gasto total con casi 1980 euros en solo 2 pedidos. María ha hecho más pedidos (3) pero ha gastado un poco menos.
LEFT JOIN con condición adicional en el ON
Puedes añadir condiciones al ON además de la relación entre claves. Esto es diferente de añadirlas al WHERE, y la diferencia importa mucho con LEFT JOIN:
SELECT
cl.nombre,
COUNT(p.id) AS pedidos_entregados
FROM clientes cl
LEFT JOIN pedidos p ON cl.id = p.cliente_id
AND p.estado = 'entregado'
GROUP BY cl.id, cl.nombre
ORDER BY pedidos_entregados DESC, cl.nombre
LIMIT 8;| nombre | pedidos_entregados |
|---|---|
| María | 2 |
| Ana | 1 |
| Carlos | 1 |
| Isabel | 1 |
| Laura | 1 |
| Pedro | 1 |
| Andrés | 0 |
| Carmen | 0 |
La condición AND p.estado = 'entregado' está en el ON, no en el WHERE. Esto significa que solo se unen los pedidos entregados, pero los clientes sin pedidos entregados siguen apareciendo (con 0). Si hubiéramos puesto esa condición en el WHERE, los clientes sin pedidos entregados desaparecerían del resultado, porque WHERE filtra después del JOIN y elimina las filas con NULL.
Esta distinción entre poner condiciones en el ON o en el WHERE es una de las trampas más comunes al usar LEFT JOIN.
LEFT JOIN vs INNER JOIN
La diferencia en una sola frase: INNER JOIN descarta las filas sin correspondencia, LEFT JOIN las conserva con NULLs.
-- INNER JOIN: solo clientes CON pedidos (19 filas)
SELECT DISTINCT cl.nombre
FROM clientes cl
INNER JOIN pedidos p ON cl.id = p.cliente_id;
-- LEFT JOIN: TODOS los clientes (20 filas, incluyendo Diego)
SELECT cl.nombre, p.id AS pedido_id
FROM clientes cl
LEFT JOIN pedidos p ON cl.id = p.cliente_id
WHERE p.id IS NULL;Usa INNER JOIN cuando solo te interesan los registros que tienen relación en ambas tablas. Usa LEFT JOIN cuando necesitas todos los registros de la tabla izquierda, tengan o no relación. El caso más típico del LEFT JOIN es el patrón de "encontrar los que no tienen" que hemos visto a lo largo de este artículo.
Practica con LEFT JOIN
Usa el editor para probar LEFT JOIN y ver las filas sin correspondencia:
En el siguiente artículo veremos RIGHT JOIN, que es el espejo del LEFT JOIN.
Escrito por Eduardo Lázaro
