JOINs en MySQL

Las bases de datos relacionales dividen la información en tablas separadas para evitar repetición. Los JOINs combinan esas tablas en una sola consulta, conectando filas que comparten datos en común.

¿Por qué existe el problema que resuelve JOIN?

Mirá cómo está diseñada nuestra base de datos. La tabla ordenes tiene una columna id_cliente, pero no tiene el nombre del cliente. ¿Por qué? Porque si guardáramos el nombre en cada orden, estaríamos repitiendo datos — y si el cliente cambia de nombre, habría que actualizar miles de filas.

En cambio, el nombre vive en la tabla clientes una sola vez, y la tabla ordenes guarda solo el número que lo identifica:

-- Tabla clientes          -- Tabla ordenes
-- id | nombre             -- id | id_cliente | total
-- 1  | Ana García         -- 1  | 3          | 450.00
-- 2  | Carlos López       -- 2  | 1          | 1200.00
-- 3  | Beatriz Torres     -- 3  | 1          | 89.00

Si consultás solo la tabla ordenes, ves id_cliente = 1 pero no sabés quién es. El JOIN "junta" las dos tablas usando esa relación:

SELECT o.id, c.nombre, o.total
FROM ordenes o
JOIN clientes c ON o.id_cliente = c.id;

-- Resultado:
-- id | nombre       | total
-- 1  | Beatriz Torres | 450.00
-- 2  | Ana García     | 1200.00
-- 3  | Ana García     | 89.00
La cláusula ON

ON o.id_cliente = c.id es la condición de unión: le dice a MySQL qué columnas usar para conectar las filas. Cada fila de ordenes se combina con la fila de clientes cuyo id coincide. Si olvidás el ON, MySQL combina todas las filas con todas (producto cartesiano) — resultado: millones de filas incorrectas.

Probá el Ejemplo 1: INNER JOIN básico

INNER JOIN — solo los que tienen coincidencia

El INNER JOIN (o simplemente JOIN) devuelve únicamente las filas que tienen coincidencia en ambas tablas. Si una orden tiene un id_cliente que no existe en clientes, esa orden no aparece. Si un cliente no tiene ninguna orden, tampoco aparece.

-- Solo aparecen las órdenes que tienen un cliente válido
SELECT
    o.id        AS nro_orden,
    c.nombre    AS cliente,
    c.ciudad,
    o.fecha,
    o.estado,
    o.total
FROM ordenes o
INNER JOIN clientes c ON o.id_cliente = c.id
ORDER BY o.fecha DESC;
INNER JOIN y JOIN son equivalentes

En MySQL podés escribir solo JOIN y es lo mismo que INNER JOIN. Muchos prefieren escribirlo explícito para dejar claro el tipo de unión que están haciendo.

LEFT JOIN — todos los de la tabla izquierda

El LEFT JOIN devuelve todas las filas de la tabla que está en el FROM (la "izquierda"), tengan o no coincidencia en la otra tabla. Para las filas sin coincidencia, las columnas de la tabla derecha aparecen como NULL.

Ejemplo concreto: si un cliente nunca hizo una orden, con INNER JOIN no aparece. Con LEFT JOIN sí aparece, con total_ordenes = 0:

-- INNER JOIN: solo clientes CON órdenes
SELECT c.nombre, COUNT(o.id) AS total_ordenes
FROM clientes c
INNER JOIN ordenes o ON c.id = o.id_cliente
GROUP BY c.id, c.nombre;

-- LEFT JOIN: TODOS los clientes, hayan comprado o no
SELECT c.nombre, COUNT(o.id) AS total_ordenes
FROM clientes c
LEFT JOIN ordenes o ON c.id = o.id_cliente
GROUP BY c.id, c.nombre
ORDER BY total_ordenes DESC;

Cuando las columnas de la tabla derecha son NULL, significa que no hubo coincidencia. Podés usar eso para encontrar registros "solos":

-- Clientes que NUNCA compraron nada
-- LEFT JOIN trae a todos; WHERE filtra solo los que tienen NULL en ordenes
SELECT c.nombre, c.email
FROM clientes c
LEFT JOIN ordenes o ON c.id = o.id_cliente
WHERE o.id IS NULL;
COALESCE para reemplazar NULLs

COALESCE(valor, alternativa) devuelve el primer valor que no sea NULL. Cuando usás SUM(o.total) con LEFT JOIN, los clientes sin órdenes dan NULL. Con COALESCE(SUM(o.total), 0) mostrás 0 en vez de nada.

Probá el Ejemplo 2: LEFT JOIN

Comparación visual de los JOINs

Tipo¿Qué incluye?Cuándo usarlo
INNER JOIN Solo filas con coincidencia en ambas tablas El caso más común. Cuando necesitás datos de ambas tablas.
LEFT JOIN Todas las filas de la tabla izquierda. Las de la derecha pueden ser NULL. Cuando necesitás todos los registros de una tabla, incluso los que no tienen relación.
RIGHT JOIN Todas las filas de la tabla derecha. Las de la izquierda pueden ser NULL. Poco usado — casi siempre se reescribe como LEFT JOIN invirtiendo las tablas.

Unir más de dos tablas

Podés encadenar tantos JOINs como necesites. Cada uno agrega una tabla más al resultado. Lo importante es que cada JOIN tenga su propio ON indicando cómo se conecta con lo que ya tenés.

En nuestra base de datos, para saber qué productos compró cada cliente hay que pasar por tres tablas: clientes → ordenes → detalle_ordenes → productos.

SELECT
    c.nombre                            AS cliente,
    o.id                                AS orden,
    p.nombre                            AS producto,
    d.cantidad,
    d.precio_unitario,
    d.cantidad * d.precio_unitario      AS subtotal
FROM detalle_ordenes d
JOIN ordenes   o ON d.id_orden    = o.id      -- detalle → orden
JOIN clientes  c ON o.id_cliente  = c.id      -- orden → cliente
JOIN productos p ON d.id_producto = p.id      -- detalle → producto
ORDER BY c.nombre, o.id;
Alias de tabla — siempre usálos

Con varias tablas, escribir el nombre completo todo el tiempo es tedioso y propenso a errores. Asignale una letra corta a cada tabla (FROM ordenes o) y usá el alias en el resto de la query (o.id, o.total). Hace el código mucho más fácil de leer.

Probá el Ejemplo 3: JOIN con cuatro tablas

Combinar JOIN con GROUP BY

Los JOINs y el GROUP BY se usan juntos constantemente en reportes reales. Primero unís las tablas para tener toda la información junta, luego agrupás para resumirla.

-- ¿Cuánto gastó cada cliente en total?
SELECT
    c.nombre                                        AS cliente,
    c.ciudad,
    COUNT(DISTINCT o.id)                            AS total_ordenes,
    SUM(d.cantidad)                                 AS productos_comprados,
    ROUND(SUM(d.cantidad * d.precio_unitario), 2)   AS gasto_total
FROM clientes c
JOIN ordenes         o ON c.id         = o.id_cliente
JOIN detalle_ordenes d ON o.id         = d.id_orden
GROUP BY c.id, c.nombre, c.ciudad
ORDER BY gasto_total DESC
LIMIT 5;
Probá el Ejemplo 4: Top clientes por gasto

Buenas Prácticas

  • Siempre poné la condición ON — olvidarla genera un producto cartesiano que puede devolver millones de filas incorrectas
  • Usá alias de tabla siempre — especialmente con más de dos tablas; evita ambigüedad y hace el código más legible
  • INNER JOIN cuando necesitás datos de ambas tablas — es el tipo más común y eficiente
  • LEFT JOIN cuando necesitás incluir registros sin relación — y usá WHERE columna IS NULL para encontrar los "solos"
  • GROUP BY en la misma query que el JOIN — podés combinarlos libremente para armar reportes completos