INNER JOIN vs LEFT JOIN: ejemplos SQL, NULL y errores frecuentes
Usa INNER JOIN para obtener solo filas coincidentes. Usa LEFT JOIN para conservar todas las filas de la tabla izquierda, aunque no exista coincidencia a la derecha. Esta decisión cambia los informes de clientes, la detección de datos ausentes y las respuestas en entrevistas.
Al mostrar clientes y pedidos, un cliente con dos pedidos aparece dos veces. Un JOIN devuelve combinaciones coincidentes, no una fila por cliente automáticamente. Un cliente sin pedidos desaparece con INNER JOIN y permanece con LEFT JOIN, con NULL en las columnas del pedido.
| Pregunta del informe | JOIN adecuado |
|---|---|
| ¿Qué clientes tienen pedidos? | INNER JOIN |
| Mostrar todos los clientes, incluidos los que no tienen pedidos | LEFT JOIN |
| Encontrar clientes sin pedidos | LEFT JOIN y comprobación de NULL |
Los resultados siguientes permiten comprobar la diferencia directamente.
Pruébalo en SQLite
Abre SQL Lab, pega esta preparación y ejecútala una vez. Después ejecuta cada consulta por separado. Los nombres de tablas y columnas se mantienen en inglés para usar el mismo SQL en todos los idiomas. La interfaz del laboratorio y las lecciones enlazadas están en inglés.
CREATE TABLE blog_customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE blog_orders (
id INTEGER PRIMARY KEY, customer_id INTEGER, status TEXT, amount INTEGER
);
INSERT INTO blog_customers VALUES (1, 'Asha'), (2, 'Luis'), (3, 'Mei');
INSERT INTO blog_orders VALUES
(101, 1, 'paid', 120), (102, 1, 'pending', 80), (103, 2, 'paid', 60);
1. INNER JOIN devuelve coincidencias
Asha tiene dos pedidos, Luis uno y Mei ninguno. La consulta devuelve tres filas. Mei no aparece porque ningún pedido cumple la condición de unión.
SELECT c.name, o.id AS order_id, o.status
FROM blog_customers AS c
INNER JOIN blog_orders AS o ON o.customer_id = c.id
ORDER BY c.id, o.id;
Resultado esperado
| name | order_id | status |
|---|---|---|
| Asha | 101 | paid |
| Asha | 102 | pending |
| Luis | 103 | paid |
2. LEFT JOIN conserva los clientes
Ahora también aparece Mei. Cuando no hay pedido coincidente, las columnas de la tabla derecha contienen NULL. Asha sigue apareciendo dos veces: conservar clientes no elimina las relaciones de uno a muchos.
SELECT c.name, o.id AS order_id, o.status
FROM blog_customers AS c
LEFT JOIN blog_orders AS o ON o.customer_id = c.id
ORDER BY c.id, o.id;
Resultado esperado
| name | order_id | status |
|---|---|---|
| Asha | 101 | paid |
| Asha | 102 | pending |
| Luis | 103 | paid |
| Mei | NULL | NULL |
NULL indica que no hay un valor coincidente; no equivale a cero ni a una cadena vacía.
3. Filtra en ON para conservar las filas sin coincidencia
Para listar todos los clientes junto a sus pedidos pagados, coloca la condición de pago en ON. Un cliente que solo tenga pedidos pendientes también permanecería, con columnas de pedido NULL. La condición determina qué pedidos pueden coincidir.
SELECT c.name, o.id AS order_id
FROM blog_customers AS c
LEFT JOIN blog_orders AS o
ON o.customer_id = c.id AND o.status = 'paid'
ORDER BY c.id, o.id;
Resultado esperado
| name | order_id |
|---|---|
| Asha | 101 |
| Luis | 103 |
| Mei | NULL |
4. WHERE puede eliminar las filas que querías conservar
Mover la misma condición a WHERE cambia la pregunta. WHERE filtra las filas después de la unión. El estado NULL de Mei no cumple status = 'paid', así que Mei desaparece. Este error hace que un LEFT JOIN produzca resultados parecidos a un INNER JOIN.
SELECT c.name, o.id AS order_id
FROM blog_customers AS c
LEFT JOIN blog_orders AS o ON o.customer_id = c.id
WHERE o.status = 'paid'
ORDER BY c.id, o.id;
Resultado esperado
| name | order_id |
|---|---|
| Asha | 101 |
| Luis | 103 |
5. Cuenta pedidos reales, no la fila conservada
COUNT(o.id) ignora el ID NULL de Mei y devuelve cero. COUNT(*) contaría la fila del cliente conservada y devolvería uno. El ID del pedido es una clave primaria no nula, por lo que sirve para contar coincidencias.
SELECT c.name, COUNT(o.id) AS order_count
FROM blog_customers AS c
LEFT JOIN blog_orders AS o ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.id;
Resultado esperado
| name | order_count |
|---|---|
| Asha | 2 |
| Luis | 1 |
| Mei | 0 |
Practica y comprueba tu respuesta
Busca clientes que no tengan pedidos pagados, aunque tengan pedidos pendientes. Parte del LEFT JOIN con el filtro de pago en ON y añade WHERE o.id IS NULL antes de ORDER BY. En estos datos, la respuesta es Mei.
Comprueba NULL sobre una clave no nula de la tabla derecha. Un campo opcional, como una nota de entrega, podría ser NULL incluso en un pedido que sí existe. No uses DISTINCT para esconder duplicados antes de entender la relación: puede ocultar una condición de unión incorrecta.
Si necesitas una fila por cliente, agrega después de la unión, como en el ejemplo del recuento. Si el informe es por pedido, conserva las filas separadas. Define qué representa una fila antes de escribir la lista de columnas.
Documentación y próximos pasos
Practica con las lecciones de INNER JOIN y LEFT/RIGHT JOIN. Estos ejemplos funcionan en SQLite y usan sintaxis disponible en PostgreSQL y MySQL. Otros tipos de JOIN y la optimización dependen del motor.
Prueba los ejemplos en SQL Lab
Copia la preparación y las consultas de esta guía en el laboratorio. Los ejemplos usan SQLite. Las lecciones y las instrucciones del laboratorio están en inglés.
Abrir SQL Lab