Skip to content

JOINs ​

Hasta ahora consultaste una tabla a la vez. Pero la información útil casi siempre está repartida: el pedido tiene el cliente_id, y el nombre del cliente está en otra tabla. Los JOINs combinan tablas usando sus claves. Es el tema que separa a quien "sabe un poco de SQL" de quien realmente lo domina, así que aquí lo veremos con diagramas y resultados de cada tipo.

Concepto ​

Un JOIN es como unir dos listas por un dato en común. Tienes una lista de pedidos con número de cliente y una libreta de clientes con su nombre; para cada pedido buscas al cliente con ese número y pegas la información en la misma fila.

txt
pedidos                            clientes
┌────┬────────────┐               ┌────┬─────────────┐
│ id │ cliente_id │──── busca ───►│ id │ nombre      │
├────┼────────────┤               ├────┼─────────────┤
│ 1  │ 1          │               │ 1  │ Ana López   │
│ 2  │ 2          │               │ 2  │ Carlos Ruiz │
└────┴────────────┘               └────┴─────────────┘
                    resultado
        ┌───────────┬────────────┬─────────────┐
        │ pedido_id │ cliente_id │ nombre      │
        ├───────────┼────────────┼─────────────┤
        │ 1         │ 1          │ Ana López   │
        │ 2         │ 2          │ Carlos Ruiz │
        └───────────┴────────────┴─────────────┘

Sintaxis general:

sql
SELECT columnas
FROM tabla_a
[INNER | LEFT | RIGHT | FULL] JOIN tabla_b
  ON tabla_a.columna = tabla_b.columna;

Los tipos de JOIN de un vistazo ​

txt
     INNER JOIN             LEFT JOIN             RIGHT JOIN            FULL JOIN
    ┌───┐ ┌───┐           ┌───┐ ┌───┐           ┌───┐ ┌───┐           ┌───┐ ┌───┐
    │ A ├█┤ B │           │███├█┤ B │           │ A ├█┤███│           │███├█┤███│
    └───┘ └───┘           └───┘ └───┘           └───┘ └───┘           └───┘ └───┘
  solo coincidencias    todo A + coincid.     todo B + coincid.      todo A y todo B
TipoDevuelveSi no hay pareja
INNER JOINSolo filas con coincidencia en ambas tablasSe descarta
LEFT JOINTodas las filas de la izquierdaColumnas de la derecha en NULL
RIGHT JOINTodas las filas de la derechaColumnas de la izquierda en NULL
FULL JOINTodas las filas de ambasNULL del lado que falte
CROSS JOINTodas las combinaciones posibles—

Recuerda los "casos especiales" de nuestros datos: el Cable USB-C no tiene categoría, la categoría Deportes no tiene productos y Lucía no tiene pedidos. Con ellos verás la diferencia entre cada JOIN.

INNER JOIN ​

sql
SELECT p.nombre AS producto, c.nombre AS categoria, p.precio
FROM productos p
INNER JOIN categorias c ON p.categoria_id = c.id
ORDER BY c.nombre, p.nombre;
productocategoriaprecio
Audífonos BluetoothElectrónica899.00
Monitor 24 pulgadasElectrónica3499.00
Teclado mecánicoElectrónica1250.00
Lámpara de escritorioHogar459.00
Taza térmicaHogar199.00
Cien años de soledadLibros289.00
Libro Aprende SQLLibros349.00

Son 7 filas: el Cable USB-C desaparece (no tiene categoría con la cual coincidir) y Deportes tampoco aparece (ningún producto la referencia).

Detalles de sintaxis:

  • productos p define un alias de tabla: escribes p.nombre en lugar de productos.nombre.
  • Cuando dos tablas tienen columnas con el mismo nombre (nombre, id), debes indicar de qué tabla es cada una.
  • JOIN a secas equivale a INNER JOIN.

Buena práctica

Usa siempre la sintaxis JOIN ... ON. La forma antigua FROM a, b WHERE a.id = b.a_id funciona, pero mezcla las condiciones de unión con los filtros y si olvidas la condición obtienes un producto cartesiano gigante.

LEFT JOIN ​

Devuelve todas las filas de la tabla izquierda, tengan o no pareja.

sql
SELECT c.nombre AS categoria, p.nombre AS producto
FROM categorias c
LEFT JOIN productos p ON p.categoria_id = c.id
ORDER BY c.id, p.id;
categoriaproducto
ElectrónicaAudífonos Bluetooth
ElectrónicaTeclado mecánico
ElectrónicaMonitor 24 pulgadas
LibrosLibro Aprende SQL
LibrosCien años de soledad
HogarLámpara de escritorio
HogarTaza térmica
DeportesNULL

Ahora Deportes sí aparece, con NULL en el producto.

Contar incluyendo los ceros ​

Caso real: "pedidos por cliente, incluidos los que no han comprado".

sql
SELECT cl.nombre, COUNT(pe.id) AS pedidos
FROM clientes cl
LEFT JOIN pedidos pe ON pe.cliente_id = cl.id
GROUP BY cl.id, cl.nombre
ORDER BY pedidos DESC, cl.nombre;
nombrepedidos
Ana López2
Carlos Ruiz1
Jorge Díaz1
María Pérez1
Lucía Gómez0

Truco clave: se usa COUNT(pe.id) y no COUNT(*). Para Lucía la fila existe (con NULL en las columnas del pedido), así que COUNT(*) daría 1; COUNT(pe.id) ignora el NULL y da 0.

Anti-join: lo que NO tiene pareja ​

sql
-- Clientes que nunca han hecho un pedido
SELECT cl.nombre, cl.email
FROM clientes cl
LEFT JOIN pedidos pe ON pe.cliente_id = cl.id
WHERE pe.id IS NULL;
nombreemail
Lucía Gómezlucia@correo.com

Muy útil para campañas de marketing ("clientes sin compras") o limpieza de datos ("categorías vacías").

Error común: filtrar la tabla derecha en el WHERE

sql
-- Intención: todos los clientes con sus pedidos ENTREGADOS (0 si no tienen)
SELECT cl.nombre, COUNT(pe.id) AS entregados
FROM clientes cl
LEFT JOIN pedidos pe ON pe.cliente_id = cl.id
WHERE pe.estado = 'entregado'          -- ¡convierte el LEFT en INNER!
GROUP BY cl.id, cl.nombre;

Solo salen Ana y Carlos: el WHERE elimina las filas con NULL. La condición sobre la tabla derecha va en el ON:

sql
SELECT cl.nombre, COUNT(pe.id) AS entregados
FROM clientes cl
LEFT JOIN pedidos pe ON pe.cliente_id = cl.id AND pe.estado = 'entregado'
GROUP BY cl.id, cl.nombre
ORDER BY cl.id;
nombreentregados
Ana López1
Carlos Ruiz1
María Pérez0
Jorge Díaz0
Lucía Gómez0

RIGHT JOIN ​

Es un LEFT JOIN al revés: conserva todas las filas de la tabla derecha.

sql
SELECT p.nombre AS producto, c.nombre AS categoria
FROM productos p
RIGHT JOIN categorias c ON p.categoria_id = c.id
ORDER BY c.id;

Da el mismo resultado que el LEFT JOIN de categorías de arriba (Deportes con producto NULL). En la práctica casi todo el mundo usa LEFT JOIN y acomoda el orden de las tablas, porque se lee más natural.

FULL OUTER JOIN ​

Conserva todas las filas de ambas tablas.

sql
-- PostgreSQL, SQL Server y SQLite 3.39+
SELECT p.nombre AS producto, c.nombre AS categoria
FROM productos p
FULL OUTER JOIN categorias c ON p.categoria_id = c.id
ORDER BY c.id, p.id;
productocategoria
Audífonos BluetoothElectrónica
Teclado mecánicoElectrónica
Monitor 24 pulgadasElectrónica
Libro Aprende SQLLibros
Cien años de soledadLibros
Lámpara de escritorioHogar
Taza térmicaHogar
NULLDeportes
Cable USB-CNULL

Aparecen ambos casos especiales: Deportes sin producto y el Cable sin categoría.

Emulación en MySQL ​

MySQL no tiene FULL JOIN. Se emula uniendo un LEFT y un RIGHT con UNION (que elimina duplicados):

sql
SELECT p.nombre AS producto, c.nombre AS categoria
FROM productos p
LEFT JOIN categorias c ON p.categoria_id = c.id
UNION
SELECT p.nombre, c.nombre
FROM productos p
RIGHT JOIN categorias c ON p.categoria_id = c.id;

UNION vs UNION ALL

UNION junta los resultados de dos consultas y elimina filas repetidas. UNION ALL las conserva (y es más rápido). Ambas consultas deben tener el mismo número de columnas con tipos compatibles.

CROSS JOIN ​

Combina cada fila de una tabla con cada fila de la otra (producto cartesiano). Si una tabla tiene 3 filas y la otra 4, salen 12.

sql
-- Generar todas las variantes de una playera
CREATE TABLE tallas  (talla VARCHAR(5));
CREATE TABLE colores (color VARCHAR(20));
INSERT INTO tallas  VALUES ('CH'), ('M'), ('G');
INSERT INTO colores VALUES ('Negro'), ('Blanco');

SELECT t.talla, c.color
FROM tallas t
CROSS JOIN colores c
ORDER BY t.talla, c.color;
tallacolor
CHBlanco
CHNegro
GBlanco
GNegro
MBlanco
MNegro

Útil para generar combinaciones, calendarios o matrices de reportes. Peligroso por accidente: dos tablas de 10,000 filas generan 100 millones.

SELF JOIN: una tabla consigo misma ​

Se une una tabla con ella misma usando dos alias. Ejemplo: parejas de clientes que viven en la misma ciudad.

sql
SELECT a.nombre AS cliente_1, b.nombre AS cliente_2, a.ciudad
FROM clientes a
JOIN clientes b
  ON a.ciudad = b.ciudad
 AND a.id < b.id;          -- evita (Ana, Ana) y parejas repetidas (Jorge, Ana)
cliente_1cliente_2ciudad
Ana LópezJorge DíazCiudad de México

El uso más común del self join es con jerarquías: una tabla empleados con columna jefe_id que apunta a otro empleado.

sql
CREATE TABLE empleados (
  id INT PRIMARY KEY,
  nombre VARCHAR(100) NOT NULL,
  jefe_id INT NULL,
  FOREIGN KEY (jefe_id) REFERENCES empleados(id)
);
INSERT INTO empleados VALUES
  (1, 'Laura (Directora)', NULL),
  (2, 'Mario (Ventas)', 1),
  (3, 'Sofía (Sistemas)', 1),
  (4, 'Iván (Vendedor)', 2);

SELECT e.nombre AS empleado, j.nombre AS jefe
FROM empleados e
LEFT JOIN empleados j ON e.jefe_id = j.id
ORDER BY e.id;
empleadojefe
Laura (Directora)NULL
Mario (Ventas)Laura (Directora)
Sofía (Sistemas)Laura (Directora)
Iván (Vendedor)Mario (Ventas)

Unir más de dos tablas ​

Los JOINs se encadenan. El detalle completo de cada pedido necesita cuatro tablas:

txt
clientes ◄── pedidos ◄── detalle_pedido ──► productos
sql
SELECT
  pe.id                              AS pedido,
  pe.fecha,
  cl.nombre                          AS cliente,
  pr.nombre                          AS producto,
  d.cantidad,
  d.precio_unitario,
  d.cantidad * d.precio_unitario     AS subtotal
FROM pedidos pe
JOIN clientes cl       ON cl.id = pe.cliente_id
JOIN detalle_pedido d  ON d.pedido_id = pe.id
JOIN productos pr      ON pr.id = d.producto_id
ORDER BY pe.id, pr.nombre;
pedidofechaclienteproductocantidadprecio_unitariosubtotal
12024-05-02Ana LópezAudífonos Bluetooth1899.00899.00
12024-05-02Ana LópezLibro Aprende SQL2349.00698.00
22024-05-10Carlos RuizTaza térmica3199.00597.00
22024-05-10Carlos RuizTeclado mecánico11250.001250.00
32024-06-01Ana LópezMonitor 24 pulgadas13499.003499.00
42024-06-15María PérezCien años de soledad1289.00289.00
42024-06-15María PérezLámpara de escritorio1459.00459.00
42024-06-15María PérezTaza térmica2199.00398.00
52024-06-20Jorge DíazAudífonos Bluetooth1899.00899.00

JOIN + GROUP BY: cuánto ha gastado cada cliente ​

sql
SELECT
  cl.nombre,
  COUNT(DISTINCT pe.id)                          AS pedidos,
  COALESCE(SUM(d.cantidad * d.precio_unitario), 0) AS total_gastado
FROM clientes cl
LEFT JOIN pedidos pe       ON pe.cliente_id = cl.id AND pe.estado <> 'cancelado'
LEFT JOIN detalle_pedido d ON d.pedido_id = pe.id
GROUP BY cl.id, cl.nombre
ORDER BY total_gastado DESC;
nombrepedidostotal_gastado
Ana López25096.00
Carlos Ruiz11847.00
María Pérez11146.00
Jorge Díaz00
Lucía Gómez00

COALESCE(..., 0) cambia el NULL de quien no compró por un 0. Jorge aparece con 0 porque su único pedido está cancelado.

Ventas por categoría ​

sql
SELECT
  COALESCE(c.nombre, 'Sin categoría')   AS categoria,
  SUM(d.cantidad)                        AS piezas,
  SUM(d.cantidad * d.precio_unitario)    AS ventas
FROM detalle_pedido d
JOIN pedidos pe      ON pe.id = d.pedido_id
JOIN productos pr    ON pr.id = d.producto_id
LEFT JOIN categorias c ON c.id = pr.categoria_id
WHERE pe.estado <> 'cancelado'
GROUP BY c.nombre
ORDER BY ventas DESC;
categoriapiezasventas
Electrónica35648.00
Hogar61454.00
Libros3987.00

USING y NATURAL JOIN ​

Si la columna se llama igual en ambas tablas puedes escribir JOIN tabla USING (columna). Evita NATURAL JOIN: une automáticamente por todas las columnas con el mismo nombre (como id o nombre) y produce resultados absurdos sin avisar.

Rendimiento

Las columnas que usas en el ON deben tener índice. Las PK ya lo tienen; en PostgreSQL las FK no se indexan solas (en MySQL/InnoDB sí). Lo verás en Índices y rendimiento.

Ejercicios ​

  1. Lista cada pedido con el nombre y la ciudad del cliente.
  2. Muestra todos los productos con el nombre de su categoría; si no tiene, muestra 'Sin categoría'.
  3. ¿Qué productos nunca se han vendido? (Pista: anti-join con detalle_pedido.)
  4. Muestra el total de cada pedido junto al nombre del cliente y el estado, ordenado por total descendente.
  5. Lista las categorías con el número de productos de cada una, incluidas las que tienen 0.
Ver solución
sql
-- 1
SELECT pe.id, pe.fecha, cl.nombre, cl.ciudad
FROM pedidos pe
JOIN clientes cl ON cl.id = pe.cliente_id
ORDER BY pe.id;
idfechanombreciudad
12024-05-02Ana LópezCiudad de México
22024-05-10Carlos RuizGuadalajara
32024-06-01Ana LópezCiudad de México
42024-06-15María PérezMonterrey
52024-06-20Jorge DíazCiudad de México
sql
-- 2
SELECT p.nombre, COALESCE(c.nombre, 'Sin categoría') AS categoria
FROM productos p
LEFT JOIN categorias c ON c.id = p.categoria_id
ORDER BY p.id;

-- 3
SELECT p.id, p.nombre
FROM productos p
LEFT JOIN detalle_pedido d ON d.producto_id = p.id
WHERE d.producto_id IS NULL;
idnombre
8Cable USB-C
sql
-- 4
SELECT pe.id, cl.nombre, pe.estado,
       SUM(d.cantidad * d.precio_unitario) AS total
FROM pedidos pe
JOIN clientes cl      ON cl.id = pe.cliente_id
JOIN detalle_pedido d ON d.pedido_id = pe.id
GROUP BY pe.id, cl.nombre, pe.estado
ORDER BY total DESC;
idnombreestadototal
3Ana Lópezenviado3499.00
2Carlos Ruizentregado1847.00
1Ana Lópezentregado1597.00
4María Pérezpendiente1146.00
5Jorge Díazcancelado899.00
sql
-- 5
SELECT c.nombre, COUNT(p.id) AS productos
FROM categorias c
LEFT JOIN productos p ON p.categoria_id = c.id
GROUP BY c.id, c.nombre
ORDER BY c.id;
nombreproductos
Electrónica3
Libros2
Hogar2
Deportes0

Resumen ​

  • INNER JOIN devuelve solo filas con pareja; LEFT JOIN conserva toda la tabla izquierda.
  • RIGHT JOIN es el espejo del LEFT; FULL JOIN conserva ambas (en MySQL se emula con UNION).
  • CROSS JOIN genera todas las combinaciones; el SELF JOIN une una tabla consigo misma (jerarquías).
  • Anti-join: LEFT JOIN ... WHERE derecha.id IS NULL encuentra lo que no tiene pareja.
  • Con LEFT JOIN usa COUNT(columna_derecha) y pon los filtros de la tabla derecha en el ON.
  • Encadena JOINs para reconstruir la información completa (cliente → pedido → detalle → producto).

Academia Web — aprende, copia y construye.