
SQL: Consultas a Múltiples Tablas con JOINs
- Dacadev
- Programación
- 17 de junio de 2026
Tabla de Contenido
Cuando una aplicación modela procesos reales, una sola tabla rara vez contiene todo el contexto. Clientes requieren direcciones, películas requieren actores y reportes operativos combinan varias entidades. Por eso, dominar consultas a múltiples tablas y entender cada JOIN en SQL es clave para escribir consultas correctas, expresivas y mantenibles.
En el artículo anterior trabajamos agrupamiento y funciones agregadas. Ahora conectaremos ese análisis con relaciones entre tablas.
Note
Aprenderás a usar INNER JOIN, unir tres o más tablas, reutilizar tablas con alias, aplicar SELF JOIN y combinar resultados con UNION, INTERSECT y EXCEPT.
Producto cartesiano
El producto cartesiano aparece cuando haces un JOIN entre dos tablas sin definir la condición que relaciona sus filas. SQL combina cada fila de la primera tabla con cada fila de la segunda.
mysql> SELECT c.first_name, c.last_name, a.address
-> FROM customer c JOIN address a;
+------------+-----------+----------------------+
| first_name | last_name | address |
+------------+-----------+----------------------+
| MARY | SMITH | 47 MySakila Drive |
| PATRICIA | JOHNSON | 47 MySakila Drive |
| LINDA | WILLIAMS | 47 MySakila Drive |
...
| FREDDIE | DUGGAN | 1325 Fukuyama Street |
| WADE | DELVALLE | 1325 Fukuyama Street |
| AUSTIN | CINTRON | 1325 Fukuyama Street |
+------------+-----------+----------------------+
361197 rows in set (0.03 sec)
El resultado tiene 361,197 filas porque customer tiene 599 registros y address tiene 603: 599 * 603 = 361197. Normalmente esto indica que falta una condición ON.
Warning
Un JOIN sin condición explícita puede multiplicar el volumen de datos y degradar el rendimiento. Revisa siempre que exista una relación clara entre las tablas.
Inner Join
El INNER JOIN retorna solo las filas que tienen coincidencia en ambas tablas según la condición definida en ON.
mysql> SELECT c.first_name, c.last_name, a.address
-> FROM customer c JOIN address a
-> ON c.address_id = a.address_id;
+-------------+--------------+----------------------------------------+
| first_name | last_name | address |
+-------------+--------------+----------------------------------------+
| MARY | SMITH | 1913 Hanoi Way |
| PATRICIA | JOHNSON | 1121 Loja Avenue |
| LINDA | WILLIAMS | 692 Joliet Street |
...
| FREDDIE | DUGGAN | 1103 Quilmes Boulevard |
| WADE | DELVALLE | 1331 Usak Boulevard |
| AUSTIN | CINTRON | 1325 Fukuyama Street |
+-------------+--------------+----------------------------------------+
599 rows in set (0.00 sec)
Aunque muchos motores interpretan JOIN como INNER JOIN, escribirlo explícitamente mejora la legibilidad cuando aparecen otros tipos de unión.
SELECT c.first_name, c.last_name, a.address
FROM customer c INNER JOIN address a
ON c.address_id = a.address_id;
Unir 3 o más tablas
Para unir más de dos tablas, repetimos el patrón: otro INNER JOIN y otra condición ON.
mysql> SELECT c.first_name, c.last_name, ct.city
-> FROM customer c
-> INNER JOIN address a
-> ON c.address_id = a.address_id
-> INNER JOIN city ct
-> ON a.city_id = ct.city_id;
+-------------+--------------+----------------------------+
| first_name | last_name | city |
+-------------+--------------+----------------------------+
| JULIE | SANCHEZ | A Corua (La Corua) |
| PEGGY | MYERS | Abha |
| TOM | MILNER | Abu Dhabi |
...
| BYRON | BOX | Zhezqazghan |
| GUY | BROWNLEE | Zhoushan |
| RONNIE | RICKETTS | Ziguinchor |
+-------------+--------------+----------------------------+
599 rows in set (0.03 sec)
Info
SQL es declarativo: describes qué resultado necesitas y el optimizador decide cómo ejecutarlo. El orden textual de las tablas no necesariamente define el orden físico del plan.
Subqueries como tablas
Una subquery puede actuar como tabla derivada dentro de un JOIN.
SELECT c.first_name, c.last_name, addr.address, addr.city
-> FROM customer c
-> INNER JOIN
-> (SELECT a.address_id, a.address, ct.city
-> FROM address a
-> INNER JOIN city ct
-> ON a.city_id = ct.city_id
-> WHERE a.district = 'California'
-> ) addr
-> ON c.address_id = addr.address_id;
+------------+-----------+------------------------+----------------+
| first_name | last_name | address | city |
+------------+-----------+------------------------+----------------+
| PATRICIA | JOHNSON | 1121 Loja Avenue | San Bernardino |
| BETTY | WHITE | 770 Bydgoszcz Avenue | Citrus Heights |
| ALICE | STEWART | 1135 Izumisano Parkway | Fontana |
...
| JACOB | LANCE | 1866 al-Qatif Avenue | El Monte |
| RENE | MCALISTER | 1895 Zhezqazghan Drive | Garden Grove |
+------------+-----------+------------------------+----------------+
9 rows in set (0.00 sec)
Aquí addr contiene direcciones de California junto con su ciudad, y luego se une con customer mediante address_id.
Misma tabla dos veces
Algunas consultas necesitan aplicar condiciones distintas sobre la misma entidad. Para eso usamos alias diferentes, incluso si la tabla física es la misma.
mysql> SELECT f.title
-> FROM film f
-> INNER JOIN film_actor fa1
-> ON f.film_id = fa1.film_id
-> INNER JOIN actor a1
-> ON fa1.actor_id = a1.actor_id
-> INNER JOIN film_actor fa2
-> ON f.film_id = fa2.film_id
-> INNER JOIN actor a2
-> ON fa2.actor_id = a2.actor_id
-> WHERE (a1.first_name = 'CATE' AND a1.last_name = 'MCQUEEN')
-> AND (a2.first_name = 'CUBA' AND a2.last_name = 'BIRCH');
+------------------+
| title |
+------------------+
| BLOOD ARGONAUTS |
| TOWERS HURRICANE |
+------------------+
2 rows in set (0.00 sec)
Esta consulta busca películas donde participaron ambos actores. Si declaramos la tabla una sola vez y usamos OR, el significado cambia: buscamos películas donde participó cualquiera de los dos.
mysql> SELECT f.title
-> FROM film f
-> INNER JOIN film_actor fa
-> ON f.film_id = fa.film_id
-> INNER JOIN actor a
-> ON fa.actor_id = a.actor_id
-> WHERE ((a.first_name = 'CATE' AND a.last_name = 'MCQUEEN')
-> OR (a.first_name = 'CUBA' AND a.last_name = 'BIRCH'));
+----------------------+
| title |
+----------------------+
| ATLANTIS CAUSE |
| BLOOD ARGONAUTS |
| COMMANDMENTS EXPRESS |
...
| VOLCANO TEXAS |
| WATERSHIP FRONTIER |
+----------------------+
54 rows in set (0.00 sec)
Self join
Un self join ocurre cuando una tabla se une consigo misma. Es común en relaciones autorreferenciales: empleados y managers, categorías padre e hijas, o películas y precuelas.
mysql> SELECT f.title, f_prnt.title prequel
-> FROM film f
-> INNER JOIN film f_prnt
-> ON f_prnt.film_id = f.prequel_film_id
-> WHERE f.prequel_film_id IS NOT NULL;
+-----------------+--------------+
| title | prequel |
+-----------------+--------------+
| FIDDLER LOST II | FIDDLER LOST |
+-----------------+--------------+
1 row in set (0.00 sec)
f representa la película actual y f_prnt representa la película relacionada como precuela. Los alias son obligatorios para evitar ambigüedad.
Teoría de conjuntos
SQL también trabaja con conjuntos completos de filas. Estas operaciones combinan resultados de múltiples consultas, siempre que las columnas sean compatibles.
La unión combina los elementos de A y B.

La intersección conserva únicamente los elementos presentes en ambos conjuntos.

La diferencia retorna los elementos de A que no están en B.

Note
Para aplicar operaciones de conjuntos, ambas consultas deben retornar el mismo número de columnas y columnas compatibles por posición.
UNION
UNION combina resultados y elimina duplicados. UNION ALL conserva todos los registros, por lo que suele ser más eficiente cuando no necesitas deduplicar.
mysql> SELECT 'CUST' typ, c.first_name, c.last_name
-> FROM customer c
-> UNION ALL
-> SELECT 'ACTR' typ, a.first_name, a.last_name
-> FROM actor a;
+------+------------+-------------+
| typ | first_name | last_name |
+------+------------+-------------+
| CUST | MARY | SMITH |
| CUST | PATRICIA | JOHNSON |
| CUST | LINDA | WILLIAMS |
...
| ACTR | MARY | KEITEL |
| ACTR | JULIA | FAWCETT |
| ACTR | THORA | TEMPLE |
+------+------------+-------------+
799 rows in set (0.00 sec)
Si duplicamos la misma consulta contra actor, UNION ALL devuelve dos copias de cada fila.
mysql> SELECT 'ACTR' typ, a.first_name, a.last_name
-> FROM actor a
-> UNION ALL
-> SELECT 'ACTR' typ, a.first_name, a.last_name
-> FROM actor a;
+------+-------------+--------------+
| typ | first_name | last_name |
+------+-------------+--------------+
| ACTR | PENELOPE | GUINESS |
| ACTR | NICK | WAHLBERG |
| ACTR | ED | CHASE |
...
| ACTR | MARY | KEITEL |
| ACTR | JULIA | FAWCETT |
| ACTR | THORA | TEMPLE |
+------+-------------+--------------+
400 rows in set (0.00 sec)
Un ejemplo más claro muestra duplicados al combinar clientes y actores con patrones similares:
mysql> SELECT c.first_name, c.last_name
-> FROM customer c
-> WHERE c.first_name LIKE 'J%' AND c.last_name LIKE 'D%'
-> UNION ALL
-> SELECT a.first_name, a.last_name
-> FROM actor a
-> WHERE a.first_name LIKE 'J%' AND a.last_name LIKE 'D%';
+------------+-----------+
| first_name | last_name |
+------------+-----------+
| JENNIFER | DAVIS |
| JENNIFER | DAVIS |
| JUDY | DEAN |
| JODIE | DEGENERES |
| JULIANNE | DENCH |
+------------+-----------+
5 rows in set (0.00 sec)
INTERSECT
INTERSECT retorna únicamente las filas presentes en ambos conjuntos. Dependiendo del motor, puede existir INTERSECT ALL, que conserva duplicados según sus reglas.
Es útil para encontrar coincidencias entre fuentes: identificadores compartidos, productos presentes en dos catálogos o usuarios existentes en dos procesos.
EXCEPT
EXCEPT retorna las filas del primer conjunto que no aparecen en el segundo. También puede existir EXCEPT ALL, según el motor.
Esta operación es práctica para auditoría y reconciliación de datos.
Warning
Algunos motores no implementan INTERSECT, EXCEPT o sus variantes ALL. En esos casos puedes modelar equivalentes con JOIN, NOT EXISTS o LEFT JOIN.
Reglas de operaciones de conjuntos
Las operaciones de conjuntos son estrictas: mismo número de columnas, tipos compatibles y mismo orden.
Ordenar resultados con alias válidos
Si ordenas el resultado final, usa los nombres expuestos por el primer SELECT. En este ejemplo, last_name y first_name ya no son etiquetas válidas porque fueron renombradas como lname y fname.
-- ERROR
mysql> SELECT a.first_name fname, a.last_name lname
-> FROM actor a
-> WHERE a.first_name LIKE 'J%' AND a.last_name LIKE 'D%'
-> UNION ALL
-> SELECT c.first_name, c.last_name
-> FROM customer c
-> WHERE c.first_name LIKE 'J%' AND c.last_name LIKE 'D%'
-> ORDER BY last_name, first_name;
ERROR 1054 (42S22): Unknown column 'last_name' in 'order clause'
-- WORKS
mysql> SELECT a.first_name fname, a.last_name lname
-> FROM actor a
-> WHERE a.first_name LIKE 'J%' AND a.last_name LIKE 'D%'
-> UNION ALL
-> SELECT c.first_name, c.last_name
-> FROM customer c
-> WHERE c.first_name LIKE 'J%' AND c.last_name LIKE 'D%'
-> ORDER BY lname, fname;
+----------+-----------+
| fname | lname |
+----------+-----------+
| JENNIFER | DAVIS |
| JENNIFER | DAVIS |
| JUDY | DEAN |
| JODIE | DEGENERES |
| JULIANNE | DENCH |
+----------+-----------+
5 rows in set (0.00 sec)
El orden de las operaciones importa
Las uniones se evalúan según el orden y la precedencia del motor. Cuando mezcles UNION, INTERSECT y EXCEPT, usa paréntesis para expresar intención y evitar resultados ambiguos.
Reglas prácticas:
- Los alias visibles provienen del primer
SELECT. UNIONelimina duplicados;UNION ALLlos conserva.ORDER BYse aplica al conjunto final, salvo que uses subqueries.- El orden de operaciones puede cambiar el resultado.
Cierre
Los JOINs y las operaciones de conjuntos resuelven problemas complementarios. INNER JOIN conecta filas relacionadas; UNION, INTERSECT y EXCEPT combinan resultados compatibles.
Dominar ambos enfoques mejora la semántica de reportes, filtros e integraciones. En el siguiente artículo avanzaremos hacia Subqueries, donde veremos subconsultas para construir lógica SQL modular.


