Los JOINs de SQL combinan filas de dos tablas haciendo coincidir una columna que comparten, como el author_id de un libro y el id de un autor. El tipo de join decide qué pasa con las filas que no coinciden. Los ejemplos funcionan en PostgreSQL, MySQL, SQLite y SQL Server.
Cómo usar los JOINs de SQL
Pon la segunda tabla después de JOIN e indica en ON cómo coinciden sus filas con las de la primera tabla:
SELECT columns
FROM table_a
JOIN table_b ON table_a.key = table_b.key;
Todos los ejemplos de esta sección unen las mismas dos tablas: Butler escribió dos de los libros, Asimov no escribió ninguno y nadie sabe quién escribió Beowulf.
INNER JOIN
Un INNER JOIN devuelve solo las filas que coinciden en ambas tablas. JOIN por sí solo significa INNER JOIN.
-- Input: authors
-- id | name
-- ---+--------
-- 1 | Le Guin
-- 2 | Butler
-- 3 | Asimov
-- Input: books
-- title | author_id
-- ---------------------+----------
-- The Dispossessed | 1
-- Kindred | 2
-- Parable of the Sower | 2
-- Beowulf | NULL
SELECT a.name, b.title
FROM authors a
INNER JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
Asimov no tiene libros y Beowulf no tiene autor, así que ninguno aparece.
LEFT JOIN
Un LEFT JOIN (o LEFT OUTER JOIN) devuelve todas las filas de la tabla izquierda, la que va después de FROM. Cuando la tabla derecha no tiene coincidencia, sus columnas vuelven como NULL.
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
-- Asimov | NULL
RIGHT JOIN
Un RIGHT JOIN (o RIGHT OUTER JOIN) es su reflejo: todas las filas de la tabla derecha, con NULLs donde la tabla izquierda no tiene coincidencia.
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a
RIGHT JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
-- NULL | Beowulf
FULL OUTER JOIN
Un FULL OUTER JOIN devuelve todas las filas de ambas tablas, emparejadas cuando es posible y rellenadas con NULLs cuando no. MySQL no lo tiene: consulta “Sintaxis alternativa” más abajo.
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a
FULL OUTER JOIN books b ON a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
-- NULL | Beowulf
-- Asimov | NULL
CROSS JOIN
Un CROSS JOIN empareja cada fila de una tabla con cada fila de la otra, por eso no lleva ON. Dos tallas y dos colores dan cuatro combinaciones; 1,000 filas por 1,000 filas dan un millón, así que úsalo con tablas pequeñas.
-- Input: sizes
-- size
-- ----
-- S
-- M
-- Input: colors
-- color
-- -----
-- Red
-- Blue
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;
-- Output:
-- size | color
-- -----+------
-- S | Red
-- S | Blue
-- M | Red
-- M | Blue
LEFT ANTI JOIN
Un left anti join devuelve las filas de la tabla izquierda que no tienen coincidencia en la tabla derecha. No existe una palabra clave para esto: haz un LEFT JOIN y quédate con las filas donde el lado derecho vino como NULL.
-- Input: the authors and books tables above
SELECT a.name
FROM authors a
LEFT JOIN books b ON a.id = b.author_id
WHERE b.author_id IS NULL;
-- Output:
-- name
-- ------
-- Asimov
RIGHT ANTI JOIN
Un right anti join devuelve las filas de la tabla derecha que no tienen coincidencia en la tabla izquierda.
-- Input: the authors and books tables above
SELECT b.title
FROM authors a
RIGHT JOIN books b ON a.id = b.author_id
WHERE a.id IS NULL;
-- Output:
-- title
-- -------
-- Beowulf
Reglas que debes conocer
-
NULL nunca coincide con nada, ni siquiera con otro NULL. El
author_idNULL de Beowulf no coincide con ningún autor, así que solo lo devuelven los joins que conservan los libros sin coincidencia. - Una fila aparece una vez por cada fila con la que coincide. Butler aparece dos veces arriba porque coincide con dos libros. Si luego sumas una columna de la tabla de autores, sus valores cuentan doble.
-
Con un outer join, una condición sobre la otra tabla va en
ON. EnWHERE, se ejecuta después del join y descarta las filas NULL que el join acababa de conservar:
-- Input: the authors and books tables above
-- Only Butler is left: the WHERE drops every author whose title is NULL
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON a.id = b.author_id
WHERE b.title LIKE 'K%';
-- Output:
-- name | title
-- -------+--------
-- Butler | Kindred
-- Every author is kept, with a book only where it starts with K
SELECT a.name, b.title
FROM authors a
LEFT JOIN books b ON a.id = b.author_id AND b.title LIKE 'K%';
-- Output:
-- name | title
-- --------+--------
-- Le Guin | NULL
-- Butler | Kindred
-- Asimov | NULL
Sintaxis alternativa
MySQL no tiene FULL OUTER JOIN. En su lugar, combina un LEFT JOIN con las filas sin coincidencia de un RIGHT JOIN:
-- MySQL
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a LEFT JOIN books b ON a.id = b.author_id
UNION ALL
SELECT a.name, b.title
FROM authors a RIGHT JOIN books b ON a.id = b.author_id
WHERE a.id IS NULL;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Parable of the Sower
-- Butler | Kindred
-- Asimov | NULL
-- NULL | Beowulf
SQLite antes de la versión 3.39 no tiene RIGHT JOIN ni FULL OUTER JOIN: invierte las tablas y usa un LEFT JOIN.
Un anti join también se puede escribir con NOT EXISTS. Evita NOT IN para esto: si la subconsulta devuelve un solo NULL, como el author_id de Beowulf aquí, NOT IN no devuelve absolutamente nada:
-- Input: the authors and books tables above
SELECT a.name
FROM authors a
WHERE NOT EXISTS (SELECT 1 FROM books b WHERE b.author_id = a.id);
-- Output:
-- name
-- ------
-- Asimov
SELECT a.name
FROM authors a
WHERE a.id NOT IN (SELECT author_id FROM books);
-- Output:
-- name
-- ----
-- (0 rows)
Cuando las columnas a comparar tienen el mismo nombre en ambas tablas, puedes escribir JOIN books USING (author_id) en lugar de la condición ON, excepto en SQL Server.
El código más antiguo suele listar las tablas separadas por comas y poner la coincidencia en WHERE. Es un INNER JOIN escrito de otra forma. Si omites el WHERE, se convierte en un CROSS JOIN:
-- Input: the authors and books tables above
SELECT a.name, b.title
FROM authors a, books b
WHERE a.id = b.author_id;
-- Output:
-- name | title
-- --------+---------------------
-- Le Guin | The Dispossessed
-- Butler | Kindred
-- Butler | Parable of the Sower
¿Cuándo deberías usar JOINs de SQL?
Usa un join siempre que los datos que necesitas estén repartidos entre varias tablas. Cuál usar depende de qué debe pasar con las filas que no coinciden, y la mayoría de los problemas caen en uno de estos cinco casos.
Buscar detalles en una tabla relacionada
Una fila de empleado guarda un ID de departamento, pero el reporte necesita el nombre del departamento. Un INNER JOIN lo obtiene y descarta cualquier fila que no tenga nada que buscar.
Conservar filas sin coincidencia
Cuando los datos relacionados son opcionales, como un empleado al que aún no se le asigna departamento, un INNER JOIN lo ocultaría sin avisar. Un LEFT JOIN conserva todas las filas de la tabla principal y llena los huecos con NULL.
Listar todos los elementos, incluso los vacíos
Los reportes suelen necesitar todos los departamentos, categorías o clientes, incluidos los que todavía no tienen nada. Haz que esa sea la tabla cuyas filas se conservan todas: primera con un LEFT JOIN, o segunda con un RIGHT JOIN.
Encontrar filas sin coincidencia
Clientes que nunca compraron, productos que nunca se vendieron, empleados sin departamento: un anti join encuentra lo que falta, lo que también lo hace una buena verificación de calidad de datos.
Generar todas las combinaciones
Un CROSS JOIN genera filas que todavía no existen: cada talla en cada color, cada empleado con cada curso de capacitación o cada tienda en cada día de un reporte.
Ejemplos reales de JOINs de SQL
Estudiantes y sus cursos
Una relación muchos a muchos necesita dos INNER JOINs, a través de la tabla intermedia que vincula estudiantes con cursos. Mei no está inscrita en nada, así que no aparece:
-- Input: students
-- id | name
-- ---+------
-- 1 | Amara
-- 2 | Lucas
-- 3 | Mei
-- Input: enrollments
-- student_id | course_id | grade
-- -----------+-----------+------
-- 1 | 10 | A
-- 1 | 20 | B
-- 2 | 10 | B
-- Input: courses
-- id | course_name
-- ---+------------
-- 10 | Databases
-- 20 | Statistics
SELECT s.name, c.course_name, e.grade
FROM students s
INNER JOIN enrollments e ON s.id = e.student_id
INNER JOIN courses c ON e.course_id = c.id;
-- Output:
-- name | course_name | grade
-- ------+-------------+------
-- Amara | Databases | A
-- Amara | Statistics | B
-- Lucas | Databases | B
Usuarios con perfiles opcionales
No todos los usuarios han llenado su perfil. Un LEFT JOIN los conserva a todos:
-- Input: users
-- id | username
-- ---+---------
-- 1 | rosa_m
-- 2 | devon
-- 3 | kai_l
-- Input: user_profiles
-- user_id | bio
-- --------+------------------------
-- 1 | Data engineer in Lisbon
-- 3 | Loves SQL puzzles
SELECT u.username, p.bio
FROM users u
LEFT JOIN user_profiles p ON u.id = p.user_id;
-- Output:
-- username | bio
-- ---------+------------------------
-- rosa_m | Data engineer in Lisbon
-- devon | NULL
-- kai_l | Loves SQL puzzles
Número de pedidos por cliente
Para incluir a los clientes sin pedidos, haz un LEFT JOIN desde customers. Luego cuenta una columna de la tabla de pedidos: COUNT(*) cuenta la fila que el LEFT JOIN inventó para Omar, así que tendría 1 en vez de 0:
-- Input: customers
-- id | name
-- ---+------------
-- 1 | Harper Lee
-- 2 | Omar Haddad
-- 3 | Yuki Tanaka
-- Input: orders
-- id | customer_id | total
-- ----+-------------+-------
-- 101 | 1 | 59.90
-- 102 | 3 | 120.00
-- 103 | 1 | 14.50
-- Wrong: COUNT(*) counts Omar's NULL row
SELECT c.name, COUNT(*) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY c.id;
-- Output:
-- name | order_count
-- ------------+------------
-- Harper Lee | 2
-- Omar Haddad | 1
-- Yuki Tanaka | 1
-- Right: COUNT(o.id) skips it
SELECT c.name, COUNT(o.id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
ORDER BY c.id;
-- Output:
-- name | order_count
-- ------------+------------
-- Harper Lee | 2
-- Omar Haddad | 0
-- Yuki Tanaka | 1
Facturas y pagos que no cuadran
Un FULL OUTER JOIN que conserva solo las filas sin coincidencia compara dos listas entre sí en una sola consulta: la factura 502 no se ha pagado y el pago 9003 corresponde a una factura que no existe:
-- Input: invoices
-- invoice_id | amount
-- -----------+-------
-- 501 | 120.00
-- 502 | 75.50
-- 503 | 300.00
-- Input: payments
-- payment_id | invoice_id | paid
-- -----------+------------+-------
-- 9001 | 501 | 120.00
-- 9002 | 503 | 300.00
-- 9003 | 504 | 42.00
-- MySQL has no FULL OUTER JOIN: use the UNION ALL form under "Alternative syntax"
SELECT i.invoice_id, i.amount, p.payment_id, p.invoice_id AS paid_invoice
FROM invoices i
FULL OUTER JOIN payments p ON p.invoice_id = i.invoice_id
WHERE i.invoice_id IS NULL OR p.payment_id IS NULL;
-- Output:
-- invoice_id | amount | payment_id | paid_invoice
-- -----------+--------+------------+-------------
-- 502 | 75.50 | NULL | NULL
-- NULL | NULL | 9003 | 504
Ventas diarias por tienda, incluidos los días sin ventas
Haz un CROSS JOIN de las tiendas con los días para obtener una fila por tienda por día, y luego un LEFT JOIN de las ventas sobre esa cuadrícula. York no vendió nada el 6 de octubre y aun así tiene su fila:
-- Input: stores
-- store
-- -----
-- Leeds
-- York
-- Input: days
-- day
-- ----------
-- 2026-10-05
-- 2026-10-06
-- Input: sales
-- store | day | amount
-- ------+------------+-------
-- Leeds | 2026-10-05 | 410.00
-- Leeds | 2026-10-06 | 385.50
-- York | 2026-10-05 | 220.00
SELECT st.store, d.day, COALESCE(SUM(s.amount), 0.00) AS takings
FROM stores st
CROSS JOIN days d
LEFT JOIN sales s ON s.store = st.store AND s.day = d.day
GROUP BY st.store, d.day
ORDER BY st.store, d.day;
-- Output:
-- store | day | takings
-- ------+------------+--------
-- Leeds | 2026-10-05 | 410.00
-- Leeds | 2026-10-06 | 385.50
-- York | 2026-10-05 | 220.00
-- York | 2026-10-06 | 0.00
Más lecturas sobre JOINs de SQL
En este blog:
- Hoja de referencia de SQL JOIN: todos los tipos de join y su diagrama en una sola página
- MySQL FULL OUTER JOIN: más formas de suplir el FULL OUTER JOIN que le falta a MySQL
- Guía de joins en MySQL: joins en MySQL, con ejemplos
- Guía de joins en SQL Server: joins en T-SQL
- PostgreSQL CROSS JOIN: más usos para consultas de todas las combinaciones
- Guía de relaciones en bases de datos: cómo se relacionan entre sí las tablas que unes
- Relaciones uno a uno, guía de relaciones uno a muchos y guía de relaciones muchos a muchos: el join que necesita cada relación
- Guía de SQL COALESCE: reemplaza los NULLs que deja un outer join
Documentación oficial:
- PostgreSQL: tablas unidas
- MySQL: cláusula JOIN
- SQLite: la cláusula FROM y los joins
- SQL Server: cláusula FROM más JOIN
Beekeeper Studio es un cliente de bases de datos gratuito y de código abierto
La mejor herramienta de consultas y edición de SQL que he usado. Tiene todo lo que necesito para administrar mi base de datos. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio es rápido, intuitivo y fácil de usar. Es compatible con muchísimas bases de datos y funciona de maravilla en Windows, Mac y Linux.
Lo que dicen los usuarios de Beekeeper Studio
"Beekeeper Studio reemplazó por completo mi antiguo flujo de trabajo con SQL. Es rápido, intuitivo y hace que trabajar con bases de datos vuelva a ser agradable."
"He probado muchas GUIs de bases de datos, pero Beekeeper logra el equilibrio perfecto entre funciones y simplicidad. Simplemente funciona."