Curso SQL · Northwind
Tema 22 de 64
Bloque 5 · Combinar tablas PostgreSQL 17 northwind

LEFT, RIGHT y FULL JOIN

Requiereinner-join

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 WHERE convierte tu LEFT JOIN en un INNER JOIN. Si la condición debe formar parte del emparejamiento, va en el ON. Si debe filtrar el resultado, va en el WHERE. 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, nunca count(*). Con count(*), 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 JOIN genera las 832 filas, y después el WHERE exige ship_country = 'Germany'. Las filas rellenas de NULL no cumplen esa condición y se van. El LEFT JOIN se ha convertido en un INNER JOIN y la palabra LEFT no 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 JOIN devuelve el mismo número de filas que el INNER JOIN, es que hay una condición sobre la tabla derecha en el WHERE.

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

  1. count(*) en un LEFT JOIN. Los que no tienen nada salen con 1. El error número uno del tema.
  2. Condición sobre la tabla derecha en el WHERE. Anula el LEFT JOIN sin decir nada.
  3. Comprobar IS NULL sobre una columna que puede ser nula de verdad. Usa la clave primaria.
  4. sum() sobre un LEFT JOIN sin coalesce. Los que no emparejan dan NULL, no 0. Un ranking ordenado por esa suma pone los NULL donde no toca.
  5. Encadenar LEFT JOIN y luego un INNER JOIN detrás. El INNER posterior descarta las filas rellenas de NULL y deshace todo el trabajo del LEFT.
  6. Usar RIGHT JOIN por costumbre. Legal, ilegible.
  7. Olvidar el coalesce en la clave de un FULL 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

  1. Todos los productos con las unidades vendidas, incluidos los que no se han vendido nunca. ¿Hay alguno?
  2. Todos los empleados con su número de pedidos, incluidos los que no gestionaron ninguno.
  3. Las categorías con su facturación total, poniendo 0 donde no haya ventas.
  4. Los proveedores que no tienen ningún producto en catálogo.
  5. 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.

? RespuestaRespóndela antes de abrir
SELECT c.customer_id,
       c.company_name,
       c.country,
       count(DISTINCT o.order_id) AS pedidos_1998,
       round(coalesce(sum(od.unit_price * od.quantity * (1 - od.discount)), 0), 2) AS facturacion_1998
FROM customers c
LEFT JOIN orders o        ON  o.customer_id = c.customer_id
                         AND o.order_date  >= '1998-01-01'
                         AND o.order_date  <  '1999-01-01'
LEFT JOIN order_details od ON od.order_id   = o.order_id
GROUP BY c.customer_id, c.company_name, c.country
ORDER BY facturacion_1998 DESC;
    company_name    | pedidos | facturacion_1998
--------------------+---------+------------------
 Ernst Handel       |       9 |         41210.65
 QUICK-Stop         |       8 |         37217.32
 Save-a-lot Markets |      11 |         36310.11
 ...
 Paris spécialités  |       0 |             0.00

Devuelve 91 filas, que era el requisito. Las tres trampas, todas vistas antes en este tema:

  1. El filtro de año va en el ON, no en el WHERE. En el WHERE habría convertido el LEFT JOIN en INNER y habría devuelto solo los clientes que compraron en 1998 — justo los que no les fallan. El informe se llama "quién nos está fallando" y el error habría borrado exactamente a esos.
  2. El segundo JOIN también tiene que ser LEFT. Un INNER JOIN con order_details descartaría las filas de los clientes sin pedidos y deshría el trabajo del primero.
  3. coalesce(sum(...), 0) y count(DISTINCT o.order_id). Sin el coalesce, los clientes sin ventas salen con NULL y el ORDER BY ... DESC los coloca arriba del todo (ordenar-y-limitar): el informe empezaría con los peores clientes fingiendo ser los mejores. Y sin el DISTINCT, la columna de pedidos contaría líneas.

Y lo que hay que decir al entregar: un cliente con 0 en 1998 puede ser de tres cosas muy distintas — uno que compraba y dejó de hacerlo (el que le interesa a Ventas), uno de alta reciente que aún no ha comprado, y uno que no ha comprado nunca. Son tres acciones comerciales opuestas y en esta tabla salen idénticos. Para separarlos hace falta una columna más: la fecha de su último pedido, sea del año que sea.

✓ SolucionesResuélvelos antes de abrir

1.

SELECT p.product_id, p.product_name, coalesce(sum(od.quantity), 0) AS unidades
FROM products p
LEFT JOIN order_details od ON od.product_id = p.product_id
GROUP BY p.product_id, p.product_name
ORDER BY unidades
LIMIT 3;
 product_id |      product_name       | unidades
------------+-------------------------+----------
          9 | Mishi Kobe Niku         |       95
         15 | Genen Shouyu            |      122
         37 | Gravad lax              |      125

No hay ninguno a cero: los 77 productos se han vendido alguna vez. El LEFT JOIN no cambia el resultado aquí, y comprobarlo es la respuesta. El coalesce está igualmente: es lo que garantiza que si mañana entra un producto nuevo, salga con 0 y no con una celda vacía.

2.

SELECT e.employee_id, e.first_name || ' ' || e.last_name AS empleado,
       count(o.order_id) AS pedidos
FROM employees e
LEFT JOIN orders o ON o.employee_id = e.employee_id
GROUP BY 1, 2
ORDER BY pedidos DESC;

Los 9 empleados tienen pedidos, así que el LEFT tampoco cambia nada. Otra vez: el valor está en haberlo comprobado, no en el resultado.

3.

SELECT cat.category_name,
       round(coalesce(sum(od.unit_price * od.quantity * (1 - od.discount)), 0), 2) AS facturacion
FROM categories cat
LEFT JOIN products      p  ON p.category_id  = cat.category_id
LEFT JOIN order_details od ON od.product_id  = p.product_id
GROUP BY cat.category_name
ORDER BY facturacion DESC;

Los dos JOIN tienen que ser LEFT. Si el segundo fuera INNER, una categoría cuyos productos no se hubieran vendido nunca desaparecería — el INNER de detrás deshace el trabajo del LEFT de delante (error 5 de la sección 4).

4.

SELECT s.supplier_id, s.company_name
FROM suppliers s
LEFT JOIN products p ON p.supplier_id = s.supplier_id
WHERE p.product_id IS NULL;

0 filas: los 29 proveedores tienen al menos un producto. Es el mismo patrón huérfano del ejemplo de clientes, mirando hacia el otro lado.

5. La primera tiene la condición en el WHERE sobre una columna de orders, así que descarta las filas rellenas de NULL y se comporta como un INNER JOIN: devuelve solo los clientes que compraron en 1998.

La segunda la tiene en el ON, así que conserva los 91 clientes y pone 0 a los que no compraron ese año.

La que responde a la pregunta es la segunda. Y la primera ni siquiera devuelve el número de clientes que dice: al filtrar por año dentro del WHERE, además pierde a los dos que no han comprado nunca.