SQL JOIN paso a paso: INNER, LEFT y cuándo usar cada uno

SQL JOIN paso a paso: INNER, LEFT y cuándo usar cada uno

JOIN en SQL paso a paso: INNER, LEFT y cuándo usar cada uno con dos tablas de ventas

Por:

Franco Brutti

·

Fecha de Actualización:

Un JOIN en SQL une filas de dos tablas que cumplen una condición, normalmente que un identificador coincida. INNER JOIN devuelve solo las filas que encuentran pareja en las dos tablas. LEFT JOIN devuelve todas las filas de la tabla de la izquierda y rellena con NULL lo que falta de la derecha. Ahí se decide qué filas desaparecen sin avisar.

En este tutorial lo vas a ver con dos tablas pequeñas de ventas: una de clientes y otra de pedidos. Los datos son inventados para el ejemplo, pero las consultas no son de pizarra: hemos ejecutado cada una el 11 de noviembre de 2026 en SQLite 3.43.2 y pegamos debajo la salida real. La sintaxis es la estándar que documenta PostgreSQL, así que te sirve en PostgreSQL y, con pocos cambios, en otros motores relacionales. Los matices que hemos encontrado los señalamos donde aparecen.

Las dos tablas de ventas del ejemplo

Para entender un JOIN necesitas dos tablas relacionadas y algún dato que no encaje. Usamos clientes (quién compra) y pedidos (qué compra y cuándo). Los pedidos apuntan al cliente con la columna cliente_id. Hay dos trampas a propósito: un cliente sin pedidos y un pedido cuyo cliente no existe.

Este es el script completo. Puedes pegarlo tal cual en SQLite, en PostgreSQL o en cualquier editor de SQL online y repetir todas las consultas del artículo:

-- Datos INVENTADOS para el ejemplo (no son ventas reales)
CREATE TABLE clientes (
  id      INTEGER PRIMARY KEY,
  nombre  TEXT NOT NULL,
  ciudad  TEXT
);

CREATE TABLE pedidos (
  id          INTEGER PRIMARY KEY,
  cliente_id  INTEGER,          -- sin FOREIGN KEY a propósito (ver el pedido 106)
  fecha       TEXT,             -- formato AAAA-MM-DD
  importe     INTEGER           -- euros
);

INSERT INTO clientes (id, nombre, ciudad) VALUES
  (1, 'Ana',    'Madrid'),
  (2, 'Bruno',  'Valencia'),
  (3, 'Carmen', 'Sevilla'),
  (4, 'David',  'Madrid');

INSERT INTO pedidos (id, cliente_id, fecha, importe) VALUES
  (101, 1, '2026-09-03', 120),
  (102, 1, '2026-10-12',  80),
  (103, 2, '2026-10-15',  45),
  (104, 3, '2026-09-18', 300),
  (105, 3, '2026-09-29',  60),
  (106, 9, '2026-10-02',  75);
-- Datos INVENTADOS para el ejemplo (no son ventas reales)
CREATE TABLE clientes (
  id      INTEGER PRIMARY KEY,
  nombre  TEXT NOT NULL,
  ciudad  TEXT
);

CREATE TABLE pedidos (
  id          INTEGER PRIMARY KEY,
  cliente_id  INTEGER,          -- sin FOREIGN KEY a propósito (ver el pedido 106)
  fecha       TEXT,             -- formato AAAA-MM-DD
  importe     INTEGER           -- euros
);

INSERT INTO clientes (id, nombre, ciudad) VALUES
  (1, 'Ana',    'Madrid'),
  (2, 'Bruno',  'Valencia'),
  (3, 'Carmen', 'Sevilla'),
  (4, 'David',  'Madrid');

INSERT INTO pedidos (id, cliente_id, fecha, importe) VALUES
  (101, 1, '2026-09-03', 120),
  (102, 1, '2026-10-12',  80),
  (103, 2, '2026-10-15',  45),
  (104, 3, '2026-09-18', 300),
  (105, 3, '2026-09-29',  60),
  (106, 9, '2026-10-02',  75);
-- Datos INVENTADOS para el ejemplo (no son ventas reales)
CREATE TABLE clientes (
  id      INTEGER PRIMARY KEY,
  nombre  TEXT NOT NULL,
  ciudad  TEXT
);

CREATE TABLE pedidos (
  id          INTEGER PRIMARY KEY,
  cliente_id  INTEGER,          -- sin FOREIGN KEY a propósito (ver el pedido 106)
  fecha       TEXT,             -- formato AAAA-MM-DD
  importe     INTEGER           -- euros
);

INSERT INTO clientes (id, nombre, ciudad) VALUES
  (1, 'Ana',    'Madrid'),
  (2, 'Bruno',  'Valencia'),
  (3, 'Carmen', 'Sevilla'),
  (4, 'David',  'Madrid');

INSERT INTO pedidos (id, cliente_id, fecha, importe) VALUES
  (101, 1, '2026-09-03', 120),
  (102, 1, '2026-10-12',  80),
  (103, 2, '2026-10-15',  45),
  (104, 3, '2026-09-18', 300),
  (105, 3, '2026-09-29',  60),
  (106, 9, '2026-10-02',  75);

Fíjate en dos filas antes de seguir. David (cliente 4) no tiene ningún pedido. Y el pedido 106 apunta al cliente 9, que no está en la tabla de clientes. En una base bien diseñada, una clave foránea (FOREIGN KEY) impediría ese pedido huérfano, pero en la vida real aparecen más de lo que parece: cargas desde Excel, clientes borrados, integraciones a medias. Son justo los casos en los que INNER y LEFT dan resultados distintos.

Qué hace un JOIN en SQL

Un JOIN combina filas de una tabla con filas de otra según una expresión que dice cuáles se emparejan. La documentación de PostgreSQL lo explica así en su tutorial Joins Between Tables: las consultas que acceden a varias tablas a la vez se llaman join queries y combinan filas indicando qué pares deben unirse.

La estructura de una consulta SQL con JOIN siempre tiene las mismas piezas:

SELECT columnas
FROM tabla_izquierda AS a
[INNER | LEFT | RIGHT | FULL] JOIN tabla_derecha AS b
  ON a.columna = b.columna;
SELECT columnas
FROM tabla_izquierda AS a
[INNER | LEFT | RIGHT | FULL] JOIN tabla_derecha AS b
  ON a.columna = b.columna;
SELECT columnas
FROM tabla_izquierda AS a
[INNER | LEFT | RIGHT | FULL] JOIN tabla_derecha AS b
  ON a.columna = b.columna;

Tres detalles que conviene fijar desde el principio:

  1. La condición va en ON. Es la regla de emparejamiento. Si ambas columnas se llaman igual, puedes usar USING (columna) como atajo; la referencia de PostgreSQL lo describe como una forma abreviada de la igualdad.

  2. INNER y OUTER son opcionales. Según esa misma referencia, JOIN a secas es un INNER JOIN, y LEFT JOIN equivale a LEFT OUTER JOIN.

  3. Pon alias y cualifica las columnas. Escribir c.nombre en vez de nombre evita ambigüedades. PostgreSQL lo considera buen estilo porque la consulta no se rompe si mañana alguien añade una columna con el mismo nombre a la otra tabla.

Quizá veas también la sintaxis antigua, con las tablas separadas por comas y la condición en el WHERE (FROM pedidos, clientes WHERE ...). Da el mismo resultado que un INNER JOIN, pero la forma explícita con JOIN ... ON, introducida en SQL-92, separa la regla de unión de los filtros y se lee mucho mejor.

INNER JOIN: solo las filas que encuentran pareja

INNER JOIN devuelve únicamente las combinaciones en las que la condición se cumple en las dos tablas. Si un pedido no tiene cliente o un cliente no tiene pedidos, esa fila no aparece. Es el JOIN por defecto y el que usarás cuando solo te interesan los datos completos, por ejemplo, para listar ventas con el nombre de quien compró.

SELECT p.id, c.nombre, p.fecha, p.importe
FROM pedidos AS p
INNER JOIN clientes AS c ON c.id = p.cliente_id
ORDER BY p.id;
SELECT p.id, c.nombre, p.fecha, p.importe
FROM pedidos AS p
INNER JOIN clientes AS c ON c.id = p.cliente_id
ORDER BY p.id;
SELECT p.id, c.nombre, p.fecha, p.importe
FROM pedidos AS p
INNER JOIN clientes AS c ON c.id = p.cliente_id
ORDER BY p.id;

Salida real:

+-----+--------+------------+---------+
| id  | nombre |   fecha    | importe |
+-----+--------+------------+---------+
| 101 | Ana    | 2026-09-03 | 120     |
| 102 | Ana    | 2026-10-12 | 80      |
| 103 | Bruno  | 2026-10-15 | 45      |
| 104 | Carmen | 2026-09-18 | 300     |
| 105 | Carmen | 2026-09-29 | 60

+-----+--------+------------+---------+
| id  | nombre |   fecha    | importe |
+-----+--------+------------+---------+
| 101 | Ana    | 2026-09-03 | 120     |
| 102 | Ana    | 2026-10-12 | 80      |
| 103 | Bruno  | 2026-10-15 | 45      |
| 104 | Carmen | 2026-09-18 | 300     |
| 105 | Carmen | 2026-09-29 | 60

+-----+--------+------------+---------+
| id  | nombre |   fecha    | importe |
+-----+--------+------------+---------+
| 101 | Ana    | 2026-09-03 | 120     |
| 102 | Ana    | 2026-10-12 | 80      |
| 103 | Bruno  | 2026-10-15 | 45      |
| 104 | Carmen | 2026-09-18 | 300     |
| 105 | Carmen | 2026-09-29 | 60

Cinco filas de seis pedidos. El 106 ha desaparecido porque el cliente 9 no existe, y David tampoco está porque no tiene pedidos. Nadie te avisa de ninguna de las dos cosas: la consulta funciona y el resultado parece correcto. Si sumaras los importes de esta salida, te saldrían 605 euros, cuando en la tabla de pedidos hay 680. Esos 75 euros de diferencia son el pedido huérfano.

LEFT JOIN: todas las filas de la tabla izquierda

LEFT JOIN conserva todas las filas de la tabla que escribes antes del JOIN, encuentren pareja o no. Si no la encuentran, las columnas de la tabla derecha salen como NULL. La documentación de PostgreSQL lo resume así: primero se hace el INNER JOIN y luego se añade cada fila de la izquierda que no casó, con nulos en la derecha.

Ahora partimos de clientes, porque la pregunta es «qué ha comprado cada cliente», incluidos los que no han comprado nada:

SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY c.id, p.id;
SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY c.id, p.id;
SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY c.id, p.id;

Salida real:

+--------+--------+---------+
| nombre | pedido | importe |
+--------+--------+---------+
| Ana    | 101    | 120     |
| Ana    | 102    | 80      |
| Bruno  | 103    | 45      |
| Carmen | 104    | 300     |
| Carmen | 105    | 60      |
| David  | NULL   | NULL

+--------+--------+---------+
| nombre | pedido | importe |
+--------+--------+---------+
| Ana    | 101    | 120     |
| Ana    | 102    | 80      |
| Bruno  | 103    | 45      |
| Carmen | 104    | 300     |
| Carmen | 105    | 60      |
| David  | NULL   | NULL

+--------+--------+---------+
| nombre | pedido | importe |
+--------+--------+---------+
| Ana    | 101    | 120     |
| Ana    | 102    | 80      |
| Bruno  | 103    | 45      |
| Carmen | 104    | 300     |
| Carmen | 105    | 60      |
| David  | NULL   | NULL

David aparece con NULL en pedido e importe. Ese NULL no significa «cero»: significa «no hay fila con la que emparejar». Y fíjate en otra cosa: Ana y Carmen salen dos veces. Un JOIN repite cada fila de la izquierda tantas veces como parejas encuentra a la derecha. Tenlo presente cuando cuentes o sumes, porque volveremos a ello.

El orden importa. clientes LEFT JOIN pedidos no es lo mismo que pedidos LEFT JOIN clientes: la tabla que manda, la que conserva todas sus filas, es siempre la que queda a la izquierda de la palabra JOIN.

Cuándo usar INNER JOIN y cuándo LEFT JOIN

Elegir entre INNER JOIN y LEFT JOIN depende de la pregunta, no de la tabla. Si habla de cosas que existen en las dos tablas («ventas con su cliente»), usa INNER. Si habla de todos los elementos de un lado, tengan o no datos en el otro («todos los clientes y lo que han comprado»), usa LEFT con ese lado a la izquierda.

Si tu pregunta es…

Usa

Tabla a la izquierda

Ventas con el nombre del cliente

INNER JOIN

pedidos (o cualquiera)

Todos los clientes y su facturación, aunque sea cero

LEFT JOIN

clientes

Clientes que nunca han comprado

LEFT JOIN + WHERE p.id IS NULL

clientes

Pedidos con un cliente que no existe

LEFT JOIN + WHERE c.id IS NULL

pedidos

Todo, case o no case, para auditar datos

FULL OUTER JOIN

da igual

Nuestro criterio para quien está empezando: ante la duda, escribe primero el LEFT JOIN desde la tabla principal del informe y mira si salen NULL. Si salen, ya sabes que hay filas sin pareja y decides con conocimiento si quieres verlas o descartarlas. Con un INNER JOIN directo, esas filas se pierden sin que te enteres, y en un informe de ventas eso suele ser dinero que no cuadra.

De tu primer JOIN a SQL avanzado

El máster en AI Data Analytics incluye el «Módulo 8 · SQL», que empieza desde cero: «el módulo SQL basics comienza desde lo básico y llega a SQL avanzado». Descarga el dossier y descubre todas las herramientas que vas a aprender a utilizar.

Descargar dossier →

Tres consultas de negocio con LEFT JOIN

Con LEFT JOIN se resuelven tres preguntas que cualquier analista de datos recibe pronto: cuánto ha comprado cada cliente, quién no ha comprado nunca y qué ventas no tienen un cliente válido. Las tres usan las mismas tablas, y cada una tiene un detalle que, si se pasa por alto, da un resultado plausible pero equivocado.

Facturación por cliente (y la trampa de COUNT(*))

Para agrupar, el LEFT JOIN se combina con GROUP BY. Hemos puesto a propósito dos formas de contar para que veas la diferencia:

SELECT c.nombre,
       COUNT(*)                   AS filas,
       COUNT(p.id)                AS pedidos,
       COALESCE(SUM(p.importe), 0) AS total
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre
ORDER BY total DESC;
SELECT c.nombre,
       COUNT(*)                   AS filas,
       COUNT(p.id)                AS pedidos,
       COALESCE(SUM(p.importe), 0) AS total
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre
ORDER BY total DESC;
SELECT c.nombre,
       COUNT(*)                   AS filas,
       COUNT(p.id)                AS pedidos,
       COALESCE(SUM(p.importe), 0) AS total
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
GROUP BY c.id, c.nombre
ORDER BY total DESC;

Salida real:

+--------+-------+---------+-------+
| nombre | filas | pedidos | total |
+--------+-------+---------+-------+
| Carmen | 2     | 2       | 360   |
| Ana    | 2     | 2       | 200   |
| Bruno  | 1     | 1       | 45    |
| David  | 1     | 0       | 0

+--------+-------+---------+-------+
| nombre | filas | pedidos | total |
+--------+-------+---------+-------+
| Carmen | 2     | 2       | 360   |
| Ana    | 2     | 2       | 200   |
| Bruno  | 1     | 1       | 45    |
| David  | 1     | 0       | 0

+--------+-------+---------+-------+
| nombre | filas | pedidos | total |
+--------+-------+---------+-------+
| Carmen | 2     | 2       | 360   |
| Ana    | 2     | 2       | 200   |
| Bruno  | 1     | 1       | 45    |
| David  | 1     | 0       | 0

Mira la fila de David. COUNT(*) cuenta filas del resultado, y el LEFT JOIN le ha dado una fila llena de NULL, así que dice 1. COUNT(p.id) cuenta valores no nulos y dice 0, que es la respuesta correcta. Por la misma razón, SUM(p.importe) de David sería NULL; COALESCE(..., 0) lo convierte en 0 para que el informe no muestre un hueco.

Clientes que nunca han comprado

Es la consulta de «clientes dormidos» para una campaña. Se hace con un LEFT JOIN y quedándote solo con las filas que no encontraron pareja:

SELECT c.nombre, c.ciudad
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
WHERE p.id IS NULL;
SELECT c.nombre, c.ciudad
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
WHERE p.id IS NULL;
SELECT c.nombre, c.ciudad
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
WHERE p.id IS NULL;

Salida real:

+--------+--------+
| nombre | ciudad |
+--------+--------+
| David  | Madrid

+--------+--------+
| nombre | ciudad |
+--------+--------+
| David  | Madrid

+--------+--------+
| nombre | ciudad |
+--------+--------+
| David  | Madrid

Comprueba siempre con IS NULL una columna que nunca pueda ser nula en una fila real, como la clave primaria p.id. Si comprobaras una columna opcional, colarías clientes que sí tienen pedidos con ese campo vacío.

Ventas sin cliente válido

La misma idea, con las tablas al revés, sirve para auditar datos: encontrar pedidos que apuntan a un cliente inexistente.

SELECT p.id, p.cliente_id, p.importe
FROM pedidos AS p
LEFT JOIN clientes AS c ON c.id = p.cliente_id
WHERE c.id IS NULL;
SELECT p.id, p.cliente_id, p.importe
FROM pedidos AS p
LEFT JOIN clientes AS c ON c.id = p.cliente_id
WHERE c.id IS NULL;
SELECT p.id, p.cliente_id, p.importe
FROM pedidos AS p
LEFT JOIN clientes AS c ON c.id = p.cliente_id
WHERE c.id IS NULL;

Salida real:

+-----+------------+---------+
| id  | cliente_id | importe |
+-----+------------+---------+
| 106 | 9          | 75

+-----+------------+---------+
| id  | cliente_id | importe |
+-----+------------+---------+
| 106 | 9          | 75

+-----+------------+---------+
| id  | cliente_id | importe |
+-----+------------+---------+
| 106 | 9          | 75

Ahí están los 75 euros que el INNER JOIN se comía. Esta consulta merece un sitio fijo en cualquier revisión de calidad de datos antes de montar un dashboard.

El error que no da error: filtrar en WHERE lo que iba en ON

Con un LEFT JOIN, poner un filtro en el ON o en el WHERE cambia el resultado. En ON se aplica al emparejar, así que la fila de la izquierda se conserva aunque no case. En WHERE se aplica después y elimina las filas con NULL, con lo que tu LEFT JOIN se convierte, en la práctica, en un INNER JOIN.

Pongamos que te piden la facturación de octubre de todos los clientes, con cero para quien no compró ese mes. Primero, con el filtro de fecha dentro del ON:

SELECT c.nombre, COALESCE(SUM(p.importe), 0) AS octubre
FROM clientes AS c
LEFT JOIN pedidos AS p
       ON p.cliente_id = c.id
      AND p.fecha >= '2026-10-01'
GROUP BY c.id, c.nombre
ORDER BY c.id;
SELECT c.nombre, COALESCE(SUM(p.importe), 0) AS octubre
FROM clientes AS c
LEFT JOIN pedidos AS p
       ON p.cliente_id = c.id
      AND p.fecha >= '2026-10-01'
GROUP BY c.id, c.nombre
ORDER BY c.id;
SELECT c.nombre, COALESCE(SUM(p.importe), 0) AS octubre
FROM clientes AS c
LEFT JOIN pedidos AS p
       ON p.cliente_id = c.id
      AND p.fecha >= '2026-10-01'
GROUP BY c.id, c.nombre
ORDER BY c.id;

Salida real:

+--------+---------+
| nombre | octubre |
+--------+---------+
| Ana    | 80      |
| Bruno  | 45      |
| Carmen | 0       |
| David  | 0

+--------+---------+
| nombre | octubre |
+--------+---------+
| Ana    | 80      |
| Bruno  | 45      |
| Carmen | 0       |
| David  | 0

+--------+---------+
| nombre | octubre |
+--------+---------+
| Ana    | 80      |
| Bruno  | 45      |
| Carmen | 0       |
| David  | 0

Ahora, la misma consulta con el filtro en el WHERE:

SELECT c.nombre, COALESCE(SUM(p.importe), 0) AS octubre
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
WHERE p.fecha >= '2026-10-01'
GROUP BY c.id, c.nombre
ORDER BY c.id;
SELECT c.nombre, COALESCE(SUM(p.importe), 0) AS octubre
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
WHERE p.fecha >= '2026-10-01'
GROUP BY c.id, c.nombre
ORDER BY c.id;
SELECT c.nombre, COALESCE(SUM(p.importe), 0) AS octubre
FROM clientes AS c
LEFT JOIN pedidos AS p ON p.cliente_id = c.id
WHERE p.fecha >= '2026-10-01'
GROUP BY c.id, c.nombre
ORDER BY c.id;

Salida real:

+--------+---------+
| nombre | octubre |
+--------+---------+
| Ana    | 80      |
| Bruno  | 45

+--------+---------+
| nombre | octubre |
+--------+---------+
| Ana    | 80      |
| Bruno  | 45

+--------+---------+
| nombre | octubre |
+--------+---------+
| Ana    | 80      |
| Bruno  | 45

Carmen y David han desaparecido. Carmen compró en septiembre, no en octubre; David no ha comprado nunca. En las dos filas, p.fecha es NULL tras el LEFT JOIN, y NULL >= '2026-10-01' no es verdadero, así que el WHERE las descarta. La documentación de PostgreSQL lo explica con un ejemplo parecido: una restricción en ON se procesa antes del JOIN y una en WHERE después, algo que no importa en un INNER JOIN pero sí, y mucho, en un JOIN externo.

Es el error que más cuesta detectar, porque la consulta funciona y los números que salen son ciertos. Simplemente faltan filas. La regla práctica: en un LEFT JOIN, las condiciones sobre la tabla de la derecha van en el ON; en el WHERE, solo las que filtran la tabla de la izquierda.

RIGHT JOIN y FULL OUTER JOIN

RIGHT JOIN es el espejo de LEFT JOIN: conserva todas las filas de la tabla de la derecha. FULL OUTER JOIN conserva las de las dos tablas y rellena con NULL los huecos de cada lado. PostgreSQL admite ambos. En SQLite llegaron tarde: según sus notas de la versión 3.39.0, de junio de 2022, hasta entonces no se admitían.

Con RIGHT JOIN, partiendo de clientes, recuperas todos los pedidos, también el huérfano:

SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes AS c
RIGHT JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY p.id;
SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes AS c
RIGHT JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY p.id;
SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes AS c
RIGHT JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY p.id;

Salida real:

+--------+--------+---------+
| nombre | pedido | importe |
+--------+--------+---------+
| Ana    | 101    | 120     |
| Ana    | 102    | 80      |
| Bruno  | 103    | 45      |
| Carmen | 104    | 300     |
| Carmen | 105    | 60      |
| NULL   | 106    | 75

+--------+--------+---------+
| nombre | pedido | importe |
+--------+--------+---------+
| Ana    | 101    | 120     |
| Ana    | 102    | 80      |
| Bruno  | 103    | 45      |
| Carmen | 104    | 300     |
| Carmen | 105    | 60      |
| NULL   | 106    | 75

+--------+--------+---------+
| nombre | pedido | importe |
+--------+--------+---------+
| Ana    | 101    | 120     |
| Ana    | 102    | 80      |
| Bruno  | 103    | 45      |
| Carmen | 104    | 300     |
| Carmen | 105    | 60      |
| NULL   | 106    | 75

Es exactamente lo mismo que escribir pedidos LEFT JOIN clientes. Por eso, en la práctica, casi todo el mundo reescribe los RIGHT JOIN como LEFT JOIN: leer siempre de izquierda a derecha, con la tabla principal delante, hace las consultas largas mucho más fáciles de revisar.

FULL OUTER JOIN muestra todo a la vez, lo que lo hace muy útil para auditar dos tablas que deberían cuadrar:

SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes AS c
FULL OUTER JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY c.id, p.id;
SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes AS c
FULL OUTER JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY c.id, p.id;
SELECT c.nombre, p.id AS pedido, p.importe
FROM clientes AS c
FULL OUTER JOIN pedidos AS p ON p.cliente_id = c.id
ORDER BY c.id, p.id;

Salida real:

+--------+--------+---------+
| nombre | pedido | importe |
+--------+--------+---------+
| NULL   | 106    | 75      |
| Ana    | 101    | 120     |
| Ana    | 102    | 80      |
| Bruno  | 103    | 45      |
| Carmen | 104    | 300     |
| Carmen | 105    | 60      |
| David  | NULL   | NULL

+--------+--------+---------+
| nombre | pedido | importe |
+--------+--------+---------+
| NULL   | 106    | 75      |
| Ana    | 101    | 120     |
| Ana    | 102    | 80      |
| Bruno  | 103    | 45      |
| Carmen | 104    | 300     |
| Carmen | 105    | 60      |
| David  | NULL   | NULL

+--------+--------+---------+
| nombre | pedido | importe |
+--------+--------+---------+
| NULL   | 106    | 75      |
| Ana    | 101    | 120     |
| Ana    | 102    | 80      |
| Bruno  | 103    | 45      |
| Carmen | 104    | 300     |
| Carmen | 105    | 60      |
| David  | NULL   | NULL

Salen las siete filas: los cinco pedidos que casan, el pedido sin cliente y el cliente sin pedidos. Un matiz si vas a repetirlo en PostgreSQL: la fila con NULL en c.id aparecerá al final, no al principio. SQLite trata NULL como el valor más pequeño al ordenar, mientras que PostgreSQL lo ordena como el más grande cuando el orden es ascendente. Si necesitas el mismo orden en los dos, añade NULLS FIRST o NULLS LAST al ORDER BY. Antes de usar FULL OUTER JOIN en otro motor, comprueba en su documentación que lo admite.

Errores frecuentes al escribir un JOIN

Los fallos habituales con JOIN casi nunca producen un mensaje de error. Producen un resultado con filas de más o de menos que parece correcto, y por eso conviene repasar esta lista antes de dar por buena cualquier consulta SQL que alimente un informe o un dashboard:

  • Olvidar la condición de unión. Si separas las tablas con comas y te olvidas del WHERE, o escribes un JOIN sin ON en un motor que lo tolera (SQLite lo hace), cada fila de una tabla se combina con todas las de la otra: un producto cartesiano. Con nuestras tablas salen 24 filas, 4 clientes por 6 pedidos; lo hemos comprobado tanto con JOIN sin ON como con CROSS JOIN. Con tablas reales de miles de filas, la consulta puede no terminar nunca.

  • Columnas sin cualificar. Si las dos tablas tienen una columna id, escribir id a secas da error o, peor, toma la que no querías. Usa siempre alias.

  • Contar con COUNT(*) tras un LEFT JOIN. Cuenta las filas con NULL como si fueran datos. Cuenta una columna de la tabla derecha, como COUNT(p.id).

  • Filtrar la tabla derecha en el WHERE. Convierte el LEFT JOIN en un INNER JOIN, como has visto con octubre.

  • Sumar después de duplicar filas. Si unes clientes con pedidos y luego sumas una columna del cliente (por ejemplo, un límite de crédito), cada cliente se suma tantas veces como pedidos tiene. Agrega antes en una subconsulta o suma solo columnas de la tabla «de muchos».

  • Confiar en que no hay huérfanos. Ejecuta la consulta de ventas sin cliente válido de vez en cuando. Si la base no tiene claves foráneas, aparecerán.

Si trabajas también con Python, la misma lógica existe en pandas: la función merge une tablas «al estilo de una base de datos», con sus variantes inner, left, right y outer. Lo explicamos con datos reales en primeros pasos con pandas en Python. Y si te preguntas por qué este tipo de consultas es propio de las bases relacionales y no de otras, lo tienes en SQL vs NoSQL.

Siguiente paso: aprender SQL de lo básico a lo avanzado

Dominar INNER y LEFT JOIN cubre buena parte de las consultas SQL que escribe un analista en su día a día. Lo siguiente es encadenar varias tablas, usar subconsultas y CTE, y funciones de ventana para comparar periodos. Ese salto es más llevadero con datos reales y alguien que revise tus consultas.

Si quieres hacerlo de forma guiada, el máster en AI Data Analytics de ThePower Tech School incluye en su plan de estudios el «Módulo 8 · SQL». Según su ficha, consultada el 11-11-2026, a la pregunta de si hace falta saber SQL avanzado antes responde: «Desde cero. Si haces especialización AE, el módulo SQL basics comienza desde lo básico y llega a SQL avanzado». La especialización Analytics Engineer, la de enfoque técnico, suma a SQL herramientas como Python, Databricks, PySpark y dbt.

Si estás valorando la profesión, te puede servir saber qué hace un data analyst y qué skills necesita y cuál es el sueldo de un analista de datos en España según las fuentes públicas.

Los datos están detrás de todas las decisiones. Aprende a leerlos.

Título + Especialización

El máster para aprender a analizar datos y convertirlos
en decisiones estratégicas.

Descubre AI Data Analytics →

Los datos están detrás de todas las decisiones. Aprende a leerlos.

Título + Especialización

El máster para aprender a analizar datos y convertirlos
en decisiones estratégicas.

Descubre AI Data Analytics →

Preguntas frecuentes sobre JOIN en SQL

¿Qué diferencia hay entre INNER JOIN y LEFT JOIN?

INNER JOIN devuelve solo las filas que encuentran pareja en las dos tablas según la condición del ON. LEFT JOIN devuelve todas las filas de la tabla de la izquierda y, cuando no encuentran pareja, rellena con NULL las columnas de la derecha. Con INNER pierdes las filas sin coincidencia; con LEFT las ves y decides qué hacer con ellas.

¿JOIN y INNER JOIN son lo mismo?

Sí. La palabra INNER es opcional y JOIN a secas equivale a INNER JOIN, tal como recoge la documentación de PostgreSQL. Lo mismo pasa con OUTER: LEFT JOIN y LEFT OUTER JOIN son idénticos. Escribir INNER explícitamente no cambia el resultado, pero ayuda a que quien lea la consulta vea de un vistazo qué tipo de unión has elegido.

¿Por qué mi JOIN devuelve filas duplicadas?

Porque cada fila se repite tantas veces como coincidencias tiene en la otra tabla. Si un cliente tiene tres pedidos, aparecerá tres veces. No es un fallo, es cómo funciona el JOIN, pero afecta a los conteos y las sumas. También puede deberse a una condición incompleta en el ON o a valores repetidos en la columna que creías única.

¿Qué es un FULL OUTER JOIN y cuándo se usa?

Es una unión que conserva todas las filas de las dos tablas: las que casan, las de la izquierda sin pareja y las de la derecha sin pareja, con NULL en los huecos. Se usa sobre todo para auditar o conciliar datos, por ejemplo, comprobar que dos sistemas tienen los mismos clientes. PostgreSQL lo admite, y SQLite desde la versión 3.39.0.

¿Cómo empiezo a aprender SQL desde cero?

Empieza por SELECT, WHERE y ORDER BY sobre una sola tabla, y pasa a JOIN en cuanto las domines, porque es lo que de verdad abre el análisis. Practica con tablas pequeñas cuyo resultado puedas comprobar a mano, como las de este artículo. SQLite no necesita instalar un servidor, así que es una buena forma de empezar hoy mismo.

Comparte este artículo

THE POWER TECH SCHOOL

¿Qué máster de Tech encaja contigo?

Cuéntanos tu punto de partida y te orientamos hacia el programa adecuado.

Pedir información

RECOMENDADOS PARA TI

Full stack con IA desde cero, sin conocimientos previos

Conviértete en Data Analyst con IA generativa

IA en producción para developers: RAG, LangGraph y MLOps

También puede interesarte

Categorías

¿Buscas algo en concreto?

Artículo siguiente

Usar IA para programar vs programar con IA: la diferencia real

Usar IA para programar vs programar con IA: la diferencia real

Hay una diferencia real entre usar IA para programar y programar con IA. Uno acelera tu flujo. El otro cambia completamente qué puedes construir.

Ver artículo

Mapas de Karnaugh: cómo simplificar funciones lógicas paso a paso

Mapas de Karnaugh: cómo simplificar funciones lógicas paso a paso

Qué es un mapa de Karnaugh, por qué el orden es 00, 01, 11, 10 y cómo agrupar bien, con un ejemplo resuelto de 4 variables y condiciones indiferentes.

Ver artículo

https://thepower.education/blog/tech/de-developer-backend-a-agentic-developer

De developer backend a Agentic Developer: hoja de ruta paso a paso

Si ya desarrollas en backend o fullstack, tienes más de la mitad del camino hecho para convertirte en Agentic Developer. Esta es la ruta concreta paso a paso.

Ver artículo

Programar con IA: el nuevo Full Stack Developer y cómo formarte

Programar con IA: el nuevo Full Stack Developer y cómo formarte

Programar con IA es más que autocompletar código. Descubre qué hace el Full Stack Developer con IA, cuánto gana en España y cómo aprender desde cero.

Ver artículo

Herramientas ciberseguridad: guía técnica

Herramientas de ciberseguridad: lo que usa un profesional para proteger su sistema

Las herramientas de ciberseguridad que usa un profesional para proteger su sistema, cómo configurarlas y cuándo usar cada una. Guía técnica para 2026.

Ver artículo

Perfil Desarrollador Agéntico: habilidades y salidas en 2026

Perfil del Desarrollador Agéntico: habilidades y salidas profesionales en 2026

Descubre qué habilidades define el perfil de un desarrollador agéntico, qué roles existen en el mercado y cuánto gana este perfil en España en 2026.

Ver artículo

En tech, quien no se forma cada año, se queda atrás
En tech, quien no se forma cada año, se queda atrás

VER MÁSTERS TECH

VER MÁSTERS TECH

Business & IA

Tech & Data

Pharma

FP Oficial

Oposiciones

Oficios

In Company