INNER JOIN
Hasta ahora, todas las consultas que hemos escrito trabajan con una sola tabla. Pero en una base de datos relacional, la información está distribuida entre varias tablas conectadas por claves foráneas. El INNER JOIN es la herramienta fundamental para combinar datos de dos o más tablas en una única consulta, basándose en una relación entre ellas.
INNER JOIN devuelve únicamente las filas que tienen correspondencia en ambas tablas. Si una fila de la primera tabla no encuentra pareja en la segunda, esa fila se descarta del resultado. Y lo mismo ocurre en sentido contrario. Esta es la característica que define al INNER JOIN y lo distingue de otros tipos de JOIN que veremos en los siguientes artículos.
Sintaxis
SELECT columnas
FROM tabla1
INNER JOIN tabla2 ON tabla1.columna = tabla2.columna;La cláusula ON especifica la condición de unión, que normalmente compara la clave foránea de una tabla con la clave primaria de otra. La palabra INNER es opcional: escribir simplemente JOIN produce exactamente el mismo resultado.
Cómo funciona
Para entender qué hace MySQL cuando ejecuta un INNER JOIN, imagina este proceso:
- MySQL toma cada fila de la primera tabla (la tabla que aparece después de
FROM, llamada tabla izquierda). - Para cada una, busca en la segunda tabla (la que aparece después de
JOIN, llamada tabla derecha) todas las filas que cumplan la condición delON. - Cuando encuentra una coincidencia, combina las columnas de ambas filas en una sola fila del resultado.
- Si no encuentra ninguna coincidencia, descarta esa fila. No aparece en el resultado.
Si una fila de la tabla izquierda coincide con varias filas de la tabla derecha, se genera una fila en el resultado por cada coincidencia. Por ejemplo, si un cliente tiene 3 pedidos, la combinación de clientes con pedidos producirá 3 filas para ese cliente.
En la práctica, MySQL usa algoritmos optimizados (índices, hash joins, nested loops), pero conceptualmente el proceso es el mismo.
JOIN entre dos tablas
El ejemplo más natural con nuestra base de datos: mostrar cada producto junto con el nombre de su categoría. La tabla productos tiene un campo categoria_id que referencia al id de la tabla categorias:
SELECT
p.nombre AS producto,
p.precio,
c.nombre AS categoria
FROM productos p
INNER JOIN categorias c ON p.categoria_id = c.id
ORDER BY c.nombre, p.nombre
LIMIT 10;| producto | precio | categoria |
|---|---|---|
| Cable USB-C a Lightning | 19.99 | Accesorios electrónicos |
| Cargador USB-C 65W | 35.99 | Accesorios electrónicos |
| Funda iPhone silicona | 49.99 | Accesorios electrónicos |
| Camiseta algodón básica | 24.99 | Camisetas |
| Camiseta técnica running | 34.99 | Camisetas |
| Robot de cocina | 249.99 | Cocina |
| Sartén antiadherente 28cm | 39.99 | Cocina |
| Banda elástica set x5 | 19.99 | Fitness |
| Esterilla yoga premium | 29.99 | Fitness |
| Mancuernas ajustables | 199.99 | Fitness |
Usamos alias de tabla (p para productos, c para categorías) para no tener que escribir el nombre completo cada vez que referenciamos una columna. La condición ON p.categoria_id = c.id conecta cada producto con su categoría correspondiente.
Como los 30 productos de nuestra base de datos tienen un categoria_id válido, todos aparecen en el resultado. Si algún producto tuviera categoria_id a NULL, no aparecería porque no encontraría ninguna coincidencia en categorias.
Observa que las 5 categorías principales (Electrónica, Ropa, Hogar, Deportes, Libros) no aparecen en este resultado. Ningún producto las referencia directamente: los productos apuntan a las subcategorías (Smartphones, Portátiles, Camisetas, etc.).
JOIN con filtro WHERE
Puedes combinar JOIN con WHERE para filtrar el resultado después de la unión. Productos de la categoría Smartphones, ordenados por precio:
SELECT
p.nombre AS producto,
p.precio,
p.stock
FROM productos p
INNER JOIN categorias c ON p.categoria_id = c.id
WHERE c.nombre = 'Smartphones'
ORDER BY p.precio DESC;| producto | precio | stock |
|---|---|---|
| iPhone 15 Pro | 1299.99 | 45 |
| Samsung Galaxy S24 | 899.99 | 62 |
| Google Pixel 8 | 699.00 | 38 |
| Xiaomi 14 | 599.99 | 80 |
MySQL primero une las dos tablas según la condición del ON y después aplica el filtro WHERE. Solo aparecen los 4 smartphones. Este patrón es muy habitual: usas el JOIN para acceder a los datos de otra tabla y el WHERE para filtrar por un valor de esa tabla.
JOIN entre tres tablas
Los pedidos están relacionados con dos tablas: clientes (quién hizo el pedido) y empleados (quién lo gestionó). Puedes encadenar varios JOINs para acceder a toda esa información en una sola consulta:
SELECT
p.id AS pedido,
cl.nombre AS cliente,
e.nombre AS empleado,
p.fecha_pedido,
p.estado,
p.total
FROM pedidos p
INNER JOIN clientes cl ON p.cliente_id = cl.id
INNER JOIN empleados e ON p.empleado_id = e.id
WHERE p.estado = 'entregado'
ORDER BY p.total DESC;| pedido | cliente | empleado | fecha_pedido | estado | total |
|---|---|---|---|---|---|
| 5 | Pedro | Natalia | 2025-11-02 10:20:00 | entregado | 1899.99 |
| 1 | María | Natalia | 2025-10-05 09:30:00 | entregado | 1349.98 |
| 2 | Carlos | Daniel | 2025-10-08 14:15:00 | entregado | 899.99 |
| 4 | María | Patricia | 2025-10-20 16:45:00 | entregado | 449.98 |
| 6 | Laura | Daniel | 2025-11-05 13:30:00 | entregado | 179.97 |
| 3 | Ana | Natalia | 2025-10-12 11:00:00 | entregado | 94.97 |
| 20 | Isabel | Daniel | 2025-12-20 11:00:00 | entregado | 74.97 |
Cada INNER JOIN añade una tabla al resultado. El primer JOIN conecta pedidos con clientes, el segundo añade empleados. Para que una fila aparezca en el resultado, debe existir coincidencia en las tres tablas. Si un pedido tuviera empleado_id a NULL (empleado desconocido), ese pedido no aparecería.
María aparece dos veces porque tiene dos pedidos entregados. Natalia gestionó los tres pedidos de mayor importe.
Detalle completo de un pedido
Podemos unir cuatro tablas para ver exactamente qué productos contiene un pedido y quién lo realizó:
SELECT
cl.nombre AS cliente,
pr.nombre AS producto,
dp.cantidad,
dp.precio_unitario,
dp.cantidad * dp.precio_unitario AS subtotal
FROM pedidos p
INNER JOIN clientes cl ON p.cliente_id = cl.id
INNER JOIN detalle_pedidos dp ON p.id = dp.pedido_id
INNER JOIN productos pr ON dp.producto_id = pr.id
WHERE p.id = 1;| cliente | producto | cantidad | precio_unitario | subtotal |
|---|---|---|---|---|
| María | iPhone 15 Pro | 1 | 1299.99 | 1299.99 |
| María | Funda iPhone silicona | 1 | 49.99 | 49.99 |
El pedido 1 de María contiene dos productos: un iPhone 15 Pro y una funda de silicona, con un total de 1349.98 euros. La tabla detalle_pedidos actúa como puente entre pedidos y productos, y almacena el precio unitario en el momento de la compra (que podría diferir del precio actual del producto).
La cadena de uniones sigue las relaciones: pedidos → detalle_pedidos (por pedido_id) → productos (por producto_id), y en paralelo pedidos → clientes (por cliente_id).
JOIN con funciones de agregación
Los JOINs se combinan naturalmente con GROUP BY para calcular estadísticas que cruzan varias tablas. Ingresos totales gestionados por cada empleado:
SELECT
e.nombre AS empleado,
e.puesto,
COUNT(*) AS pedidos,
SUM(p.total) AS ingresos
FROM pedidos p
INNER JOIN empleados e ON p.empleado_id = e.id
GROUP BY e.id, e.nombre, e.puesto
ORDER BY ingresos DESC;| empleado | puesto | pedidos | ingresos |
|---|---|---|---|
| Natalia | Vendedora Senior | 10 | 6422.89 |
| Daniel | Vendedor | 8 | 3148.88 |
| Patricia | Vendedora | 7 | 1753.88 |
Natalia lidera tanto en número de pedidos (10) como en ingresos generados (6422.89 euros). Sin el JOIN, solo veríamos los IDs de empleado en lugar de sus nombres y puestos.
Observa que agrupamos por e.id además de por nombre y puesto. Esto garantiza que si dos empleados tuvieran el mismo nombre, se contarían por separado.
Ingresos por categoría de producto
Un ejemplo más elaborado que cruza cuatro tablas: los ingresos reales por categoría, calculados a partir de los detalles de cada pedido:
SELECT
c.nombre AS categoria,
SUM(dp.cantidad) AS unidades_vendidas,
ROUND(SUM(dp.cantidad * dp.precio_unitario), 2) AS ingresos
FROM detalle_pedidos dp
INNER JOIN productos pr ON dp.producto_id = pr.id
INNER JOIN categorias c ON pr.categoria_id = c.id
GROUP BY c.nombre
ORDER BY ingresos DESC
LIMIT 5;| categoria | unidades_vendidas | ingresos |
|---|---|---|
| Portátiles | 3 | 4847.99 |
| Smartphones | 4 | 3498.97 |
| Muebles | 2 | 798.00 |
| Cocina | 3 | 539.97 |
| Running | 3 | 409.97 |
Los portátiles generan los mayores ingresos a pesar de vender solo 3 unidades, gracias a su alto precio unitario. Los smartphones venden más unidades pero a un precio medio menor.
La sintaxis antigua: JOIN implícito
Antes de que SQL introdujera la sintaxis JOIN ... ON, las tablas se unían listándolas en el FROM separadas por comas y especificando la condición en el WHERE:
-- Sintaxis antigua (JOIN implícito)
SELECT p.nombre, c.nombre AS categoria
FROM productos p, categorias c
WHERE p.categoria_id = c.id;
-- Sintaxis moderna (JOIN explícito), equivalente
SELECT p.nombre, c.nombre AS categoria
FROM productos p
INNER JOIN categorias c ON p.categoria_id = c.id;Ambas consultas producen el mismo resultado. Sin embargo, la sintaxis con JOIN ... ON es preferible por varias razones: separa claramente la condición de unión (ON) de los filtros de datos (WHERE), hace que la consulta sea más legible conforme aumenta el número de tablas, y reduce el riesgo de olvidar la condición de unión (lo que con la sintaxis antigua produciría un producto cartesiano accidental).
Cuándo se pierden filas
Es importante recordar que INNER JOIN solo devuelve filas con coincidencia en ambos lados. En nuestra base de datos, las 5 categorías principales (Electrónica, Ropa, Hogar, Deportes, Libros) no tienen productos asignados directamente. Si hicieras un INNER JOIN entre categorías y productos, esas 5 categorías no aparecerían en el resultado.
Del mismo modo, si un cliente no ha hecho ningún pedido, no aparecerá en un INNER JOIN entre clientes y pedidos. Si necesitas incluir filas sin correspondencia, necesitas un LEFT JOIN, que veremos en el siguiente artículo.
Practica con INNER JOIN
Usa el editor para unir tablas. Prueba a combinar productos con categorías, o clientes con pedidos:
Escrito por Eduardo Lázaro
