1. Qué es
El INNER JOIN descarta lo que no encuentra pareja. Los OUTER JOIN conservan esas filas y rellenan con NULL el lado que falta.
| Tipo | Conserva |
|---|---|
INNER JOIN |
Solo lo que empareja |
LEFT JOIN |
Todo lo de la izquierda + lo que empareja de la derecha |
RIGHT JOIN |
Todo lo de la derecha + lo que empareja de la izquierda |
FULL JOIN |
Todo de las dos, empareje o no |
La palabra OUTER es opcional: LEFT JOIN y LEFT OUTER JOIN son lo mismo.
La pregunta que decide cuál usar no es técnica: ¿quiero saber también de los que no tienen nada? Los clientes que no compraron, los productos que no se vendieron, los meses sin ventas. Casi siempre son las filas más interesantes del informe, y el INNER JOIN las borra.
2. Sintaxis
FROM izquierda l
LEFT JOIN derecha r ON r.clave = l.clave
Y la regla que hay que interiorizar antes que ninguna otra:
Una condición sobre la tabla derecha en el
WHEREconvierte tuLEFT JOINen unINNER JOIN. Si la condición debe formar parte del emparejamiento, va en elON. Si debe filtrar el resultado, va en elWHERE. No son intercambiables.
3. Ejemplos sobre Northwind
El caso base — todos los clientes, compren o no:
SELECT count(*) FROM customers c JOIN orders o ON o.customer_id = c.customer_id; -- 830
SELECT count(*) FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id; -- 832
832 = 830 pedidos + 2 clientes sin ningún pedido. Esos dos clientes aparecen como una fila con todas las columnas de orders en NULL.
3.1 count(*) contra count(columna) — aquí es donde importa de verdad
SELECT c.customer_id,
c.company_name,
count(o.order_id) AS pedidos,
count(*) AS filas
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
GROUP BY 1, 2
ORDER BY pedidos
LIMIT 5;
customer_id | company_name | pedidos | filas
-------------+--------------------------------------+---------+-------
PARIS | Paris spécialités | 0 | 1
FISSA | FISSA Fabrica Inter. Salchichas S.A. | 0 | 1
CENTC | Centro comercial Moctezuma | 1 | 1
LAZYK | Lazy K Kountry Store | 2 | 2
GROSR | GROSELLA-Restaurante | 2 | 2
Mira las dos primeras filas: pedidos = 0 pero filas = 1.
count(*) cuenta filas, y el LEFT JOIN produjo una fila para cada cliente sin pedidos — con todo en NULL, pero una fila. count(o.order_id) cuenta valores no nulos, y ahí no hay ninguno.
Regla sin excepción: en un
LEFT JOIN, cuenta una columna de la tabla derecha, nuncacount(*). Concount(*), los clientes que no han comprado nada aparecen con 1 pedido. Es un error que no da ningún aviso y que sale en producción.
3.2 Encontrar los huérfanos
El patrón LEFT JOIN ... WHERE derecha IS NULL es la forma clásica de responder "¿quiénes no tienen?":
SELECT c.customer_id, c.company_name, c.country
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
customer_id | company_name | country
-------------+--------------------------------------+---------
PARIS | Paris spécialités | France
FISSA | FISSA Fabrica Inter. Salchichas S.A. | Spain
Ahí están los dos clientes que el INNER JOIN del tema anterior se comía. Y ahora tienen nombre: dos empresas dadas de alta que nunca han comprado. Para Ventas, esa lista de dos filas puede valer más que el ranking de los 89 que sí compran.
Un detalle importante del WHERE: hay que comprobar IS NULL sobre una columna que nunca sea nula en la tabla derecha — la clave primaria (o.order_id) es la elección segura. Si comprobaras o.shipped_date IS NULL, saldrían también los 21 pedidos sin enviar, que sí existen. El NULL de "no hay pareja" y el NULL de "el dato falta" se escriben igual (null-y-la-logica-de-tres-valores).
Esta forma tiene un nombre y una alternativa más clara — es un anti-join, y NOT EXISTS lo dice mejor: anti-join-y-semi-join.
3.3 El error que anula el LEFT JOIN
Estas dos consultas se parecen mucho y no tienen nada que ver:
-- A: la condición en el WHERE
SELECT count(*) FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE o.ship_country = 'Germany'; -- 122
-- B: la condición en el ON
SELECT count(*) FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
AND o.ship_country = 'Germany'; -- 202
122 contra 202.
- En A, el
LEFT JOINgenera las 832 filas, y después elWHEREexigeship_country = 'Germany'. Las filas rellenas deNULLno cumplen esa condición y se van. ElLEFT JOINse ha convertido en unINNER JOINy la palabraLEFTno pinta nada. - En B, la condición forma parte del emparejamiento: "emparéjame solo con los pedidos alemanes". Los 91 clientes siguen ahí; los que no tienen pedidos alemanes salen con
NULL. 202 = 122 pedidos alemanes + 80 clientes sin ninguno.
Las dos son SQL correcto. Responden a preguntas distintas:
| Consulta | Pregunta |
|---|---|
A (WHERE) |
"Los pedidos enviados a Alemania" |
B (ON) |
"Todos los clientes, con sus pedidos alemanes si los tienen" |
Si el informe es "clientes y cuántos pedidos alemanes hizo cada uno, incluidos los que no hicieron ninguno", solo B sirve. Y es la que casi nadie escribe la primera vez.
Diagnóstico rápido: si tu
LEFT JOINdevuelve el mismo número de filas que elINNER JOIN, es que hay una condición sobre la tabla derecha en elWHERE.
3.4 RIGHT JOIN
Es un LEFT JOIN con las tablas al revés:
SELECT count(*) FROM orders o RIGHT JOIN customers c ON o.customer_id = c.customer_id; -- 832
SELECT count(*) FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id; -- 832
Idénticos. Y por eso RIGHT JOIN casi no se usa: se lee peor. Un FROM se lee de arriba abajo, y RIGHT obliga a saltar mentalmente a la última línea para saber cuál es la tabla que manda.
Convención práctica: escribe siempre LEFT y pon primero la tabla que quieres conservar entera. El único caso donde RIGHT es cómodo es cuando ya tienes tres LEFT JOIN encadenados y necesitas conservar la cuarta tabla — y ahí suele ser mejor reordenar el FROM.
3.5 FULL JOIN
Conserva las dos partes. Sobre customers y orders no aporta nada:
SELECT count(*) FROM customers c FULL JOIN orders o ON o.customer_id = c.customer_id; -- 832
Los mismos 832 que el LEFT JOIN: no hay ningún pedido huérfano, porque una clave foránea lo impide (restricciones). En una base bien diseñada, el FULL JOIN sobre una relación con FK nunca añade nada.
Donde sí sirve es comparando dos conjuntos independientes. ¿En qué países tenemos clientes, en cuáles proveedores, y en cuáles las dos cosas?
SELECT coalesce(c.country, s.country) AS pais,
(c.country IS NOT NULL) AS tiene_clientes,
(s.country IS NOT NULL) AS tiene_proveedores
FROM (SELECT DISTINCT country FROM customers) c
FULL JOIN (SELECT DISTINCT country FROM suppliers) s ON s.country = c.country
WHERE c.country IS NULL OR s.country IS NULL
ORDER BY 1;
pais | tiene_clientes | tiene_proveedores
-------------+----------------+-------------------
Argentina | t | f
Australia | f | t
Austria | t | f
Ireland | t | f
Japan | f | t
Netherlands | f | t
Singapore | f | t
...
25 países en total: 12 con las dos cosas, 9 solo con clientes y 4 solo con proveedores. Ese WHERE ... IS NULL OR ... IS NULL deja justo los 13 desajustes.
Fíjate en el coalesce de la primera columna: en un FULL JOIN ninguna de las dos columnas de la clave está siempre rellena, así que hay que combinarlas para tener el valor. Es el detalle que se olvida siempre.
Los tres usos reales del FULL JOIN: conciliar dos sistemas (lo que hay en A y no en B, y viceversa), auditar una migración, y comparar dos periodos donde alguna categoría puede faltar en uno de los dos.
4. Errores comunes
count(*)en unLEFT JOIN. Los que no tienen nada salen con 1. El error número uno del tema.- Condición sobre la tabla derecha en el
WHERE. Anula elLEFT JOINsin decir nada. - Comprobar
IS NULLsobre una columna que puede ser nula de verdad. Usa la clave primaria. sum()sobre unLEFT JOINsincoalesce. Los que no emparejan danNULL, no0. Un ranking ordenado por esa suma pone losNULLdonde no toca.- Encadenar
LEFT JOINy luego unINNER JOINdetrás. ElINNERposterior descarta las filas rellenas deNULLy deshace todo el trabajo delLEFT. - Usar
RIGHT JOINpor costumbre. Legal, ilegible. - Olvidar el
coalesceen la clave de unFULL JOIN.
5. Diferencias con SQL Server
Los OUTER JOIN son idénticos en ambos motores: LEFT, RIGHT, FULL, con OUTER opcional. Se copian y pegan sin tocar nada.
Lo único que cambia es la sintaxis antigua, que conviene reconocer porque aparece en código viejo:
| PostgreSQL | SQL Server | |
|---|---|---|
LEFT JOIN ... ON |
✅ | ✅ |
| Sintaxis antigua | ❌ Nunca existió | WHERE a.id *= b.id — eliminada desde SQL Server 2012 |
| Nulos en el resultado | Iguales | Iguales |
Si te encuentras un *= o un =*, es un LEFT/RIGHT JOIN de los años noventa. Ya no se ejecuta en ningún SQL Server moderno, así que hay que reescribirlo.
Y una diferencia práctica que sí importa: ISNULL() de SQL Server acepta dos argumentos y coalesce() acepta los que quieras. Al portar un LEFT JOIN con relleno de nulos, coalesce es la forma que funciona en los dos.
6. Ejercicios
- Todos los productos con las unidades vendidas, incluidos los que no se han vendido nunca. ¿Hay alguno?
- Todos los empleados con su número de pedidos, incluidos los que no gestionaron ninguno.
- Las categorías con su facturación total, poniendo
0donde no haya ventas. - Los proveedores que no tienen ningún producto en catálogo.
- Explica por qué estas dos dan resultados distintos y cuál responde a "clientes con sus pedidos de 1998, incluidos los que no compraron ese año": ```sql SELECT c.company_name, count(o.order_id) FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id WHERE extract(year FROM o.order_date) = 1998 GROUP BY 1;
SELECT c.company_name, count(o.order_id) FROM customers c LEFT JOIN orders o ON o.customer_id = c.customer_id AND extract(year FROM o.order_date) = 1998 GROUP BY 1; ```
7. Pregunta de negocio
Ventas quiere la lista completa de clientes con su facturación de 1998, ordenada de mayor a menor, "para ver quién nos está fallando este año". Insisten en que tienen que salir los 91.
Hay tres trampas y ya has visto las tres por separado.