
SQL: Subqueries y Tipos de JOIN Avanzados
- Dacadev
- Programación
- 20 de junio de 2026
Tabla de Contenido
Cuando tus consultas SQL empiezan a mezclar filtros, agregaciones y relaciones entre tablas, los JOIN básicos dejan de ser suficientes para expresar ciertas reglas de negocio con claridad. Las subqueries o subconsultas en SQL permiten encapsular una consulta dentro de otra, reutilizar resultados intermedios y construir filtros más precisos sin mover lógica innecesaria a la aplicación. En el artículo anterior vimos consultas a múltiples tablas con JOINs; ahora vamos a extender esa base con subconsultas, EXISTS y tipos de JOIN más avanzados.
Note
En este artículo aprenderás a usar subqueries escalares, subconsultas de múltiples filas, subqueries correlacionadas, el operador EXISTS, LEFT OUTER JOIN, CROSS JOIN y NATURAL JOIN.
Qué es una subquery en SQL
Una subquery es una consulta contenida dentro de otra sentencia SQL. A la sentencia externa la llamaremos consulta contenedora o consulta principal. La subquery siempre va entre paréntesis y, en muchos motores, se evalúa antes de que la consulta principal termine de resolver su filtro o su conjunto de datos.
Como cualquier consulta, una subquery puede retornar diferentes formas de resultado:
- Una sola fila con una sola columna.
- Múltiples filas con una sola columna.
- Múltiples filas con múltiples columnas.
Analicemos un primer ejemplo. Queremos obtener el cliente con el customer_id más alto:
mysql> SELECT customer_id, first_name, last_name
-> FROM customer
-> WHERE customer_id = (SELECT MAX(customer_id) FROM customer);
+-------------+------------+-----------+
| customer_id | first_name | last_name |
+-------------+------------+-----------+
| 599 | AUSTIN | CINTRON |
+-------------+------------+-----------+
1 row in set (0.27 sec)
La subquery (SELECT MAX(customer_id) FROM customer) retorna un único valor. Por eso podemos compararla con = en el WHERE de la consulta principal.
Info
Cuando una subquery retorna una fila y una sola columna, se comporta como una subquery escalar. Puedes usarla con operadores como =, <>, !=, >, >=, < y <=.
Tipos de subqueries
No todas las subconsultas tienen el mismo comportamiento. La diferencia más importante está en si la subquery depende o no de la consulta principal.
Subqueries no correlacionadas
Las subqueries no correlacionadas no dependen directamente de la consulta contenedora. Puedes ejecutarlas por separado, inspeccionar su resultado y luego entender cómo ese resultado alimenta el WHERE, HAVING o incluso el FROM de la consulta principal.
El ejemplo anterior es una subquery no correlacionada. Observemos otro caso: obtener todas las ciudades cuyo país no sea India.
mysql> SELECT city_id, city
-> FROM city
-> WHERE country_id <>
-> (SELECT country_id FROM country WHERE country = 'India');
+---------+----------------------------+
| city_id | city |
+---------+----------------------------+
| 1 | A Corua (La Corua) |
| 2 | Abha |
| 3 | Abu Dhabi |
| 4 | Acua |
...
| 599 | Zhoushan |
| 600 | Ziguinchor |
+---------+----------------------------+
540 rows in set (0.02 sec)
Aquí la subquery retorna el country_id asociado a India. La consulta principal usa ese valor para excluir todas las ciudades que pertenezcan a ese país.
Warning
Si usas = con una subquery que retorna más de un valor, el motor no puede comparar un valor escalar contra un conjunto de filas. En ese caso necesitas operadores como IN, ANY, ALL o reestructurar la consulta.
Subqueries de múltiples filas y una columna
Cuando una subquery retorna varias filas pero una sola columna, IN suele ser el operador adecuado. Este operador compara el valor del lado izquierdo contra el conjunto de valores retornado por la subquery.
Consideremos una consulta para obtener ciudades de Canadá o México:
mysql> SELECT city_id, city
-> FROM city
-> WHERE country_id IN
-> (SELECT country_id
-> FROM country
-> WHERE country IN ('Canada','Mexico'));
+---------+----------------------------+
| city_id | city |
+---------+----------------------------+
| 179 | Gatineau |
| 196 | Halifax |
| 300 | Lethbridge |
| 313 | London |
...
| 556 | Uruapan |
| 595 | Zapopan |
+---------+----------------------------+
37 rows in set (0.00 sec)
La subquery retorna los country_id de ambos países. Luego IN filtra las ciudades cuyo country_id esté dentro de ese conjunto.
Subqueries multicolumna
Las subqueries multicolumna retornan más de una columna y son útiles cuando necesitas comparar combinaciones de valores o enriquecer la lógica de selección. En SQL también es común resolver este tipo de necesidad con varias subqueries coordinadas, especialmente cuando cada filtro opera sobre una dimensión diferente.
Observemos este caso: queremos encontrar pares actor_id y film_id donde el actor tenga apellido MONROE y la película tenga clasificación PG.
mysql> SELECT fa.actor_id, fa.film_id
-> FROM film_actor fa
-> WHERE fa.actor_id IN
-> (SELECT actor_id FROM actor WHERE last_name = 'MONROE')
-> AND fa.film_id IN
-> (SELECT film_id FROM film WHERE rating = 'PG');
+----------+---------+
| actor_id | film_id |
+----------+---------+
| 120 | 63 |
| 120 | 144 |
| 120 | 414 |
...
| 178 | 983 |
+----------+---------+
11 rows in set (0.00 sec)
Esta consulta combina dos filtros independientes: uno sobre actores y otro sobre películas. El resultado final sale de la tabla intermedia film_actor, que representa la relación muchos-a-muchos.
Subqueries correlacionadas
Hasta ahora, las subqueries han sido independientes. Una subquery correlacionada, en cambio, depende de la consulta principal porque referencia una o más columnas de la fila que se está evaluando afuera.
Esto cambia el modelo mental: la subquery no se ejecuta una sola vez para producir un conjunto estático, sino que puede evaluarse por cada fila candidata de la consulta externa. Por eso conviene revisar su costo, especialmente en tablas grandes.
Analicemos una consulta que obtiene clientes con exactamente 20 rentas:
mysql> SELECT c.first_name, c.last_name
-> FROM customer c
-> WHERE 20 =
-> (SELECT count(*) FROM rental r
-> WHERE r.customer_id = c.customer_id);
+------------+-------------+
| first_name | last_name |
+------------+-------------+
| LAUREN | HUDSON |
| JEANETTE | GREENE |
| TARA | RYAN |
| WILMA | RICHARDS |
...
| ALFREDO | MCADAMS |
+------------+-------------+
15 rows in set (0.01 sec)
La parte clave es r.customer_id = c.customer_id. La subquery necesita el customer_id de la fila actual de customer, por eso está correlacionada.
El operador EXISTS
El operador EXISTS valida si la subquery retorna al menos una fila. No importa qué columnas seleccione la subquery; lo relevante es la existencia de registros que cumplan la condición.
Por convención, muchas consultas usan SELECT 1 dentro de EXISTS para indicar que el valor retornado no es importante:
mysql> SELECT c.first_name, c.last_name
-> FROM customer c
-> WHERE EXISTS
-> (SELECT 1 FROM rental r
-> WHERE r.customer_id = c.customer_id
-> AND date(r.rental_date) < '2005-05-25');
+------------+-------------+
| first_name | last_name |
+------------+-------------+
| CHARLOTTE | HUNTER |
| DELORES | HANSEN |
| MINNIE | ROMERO |
...
| NELSON | CHRISTENSON |
+------------+-------------+
8 rows in set (0.03 sec)
EXISTS es especialmente expresivo cuando la pregunta de negocio es binaria: “incluye esta fila si existe al menos una relación que cumple la condición”.
Tip
Usa EXISTS cuando quieras comprobar existencia y IN cuando quieras comparar contra un conjunto concreto de valores. Ambos pueden ser optimizados por el motor, pero expresan intenciones distintas.
JOINs avanzados: revisitando las uniones
Hasta este punto de la serie hemos trabajado principalmente con INNER JOIN. Ese tipo de unión retorna únicamente las filas que cumplen la condición en ambas tablas. Sin embargo, SQL ofrece variantes útiles para preservar filas sin coincidencias, generar combinaciones cartesianas o delegar la condición de unión al motor.
Outer joins
Los outer joins retornan las filas que cumplen la condición de unión y, además, conservan filas de una tabla aunque no tengan coincidencias del otro lado. En un LEFT OUTER JOIN, se preservan todas las filas de la tabla de la izquierda.
Observemos un conteo de copias por película:
mysql> SELECT f.film_id, f.title, count(i.inventory_id) num_copies
-> FROM film f
-> LEFT OUTER JOIN inventory i
-> ON f.film_id = i.film_id
-> GROUP BY f.film_id, f.title;
+---------+-----------------------------+------------+
| film_id | title | num_copies |
+---------+-----------------------------+------------+
| 1 | ACADEMY DINOSAUR | 8 |
| 2 | ACE GOLDFINGER | 3 |
| 3 | ADAPTATION HOLES | 4 |
| 4 | AFFAIR PREJUDICE | 7 |
...
| 14 | ALICE FANTASIA | 0 |
...
| 999 | ZOOLANDER FICTION | 5 |
| 1000 | ZORRO ARK | 8 |
+---------+-----------------------------+------------+
1000 rows in set (0.01 sec)
ALICE FANTASIA aparece con 0 copias porque no tiene registros asociados en inventory. Aun así, el LEFT OUTER JOIN conserva la película porque film está a la izquierda.
Veamos la misma situación sin agregación:
mysql> SELECT f.film_id, f.title, i.inventory_id
-> FROM film f
-> LEFT OUTER JOIN inventory i
-> ON f.film_id = i.film_id
-> WHERE f.film_id BETWEEN 13 AND 15;
+---------+----------------+--------------+
| film_id | title | inventory_id |
+---------+----------------+--------------+
| 13 | ALI FOREVER | 67 |
| 13 | ALI FOREVER | 68 |
| 13 | ALI FOREVER | 69 |
| 13 | ALI FOREVER | 70 |
| 14 | ALICE FANTASIA | NULL |
| 15 | ALIEN CENTER | 71 |
| 15 | ALIEN CENTER | 72 |
| 15 | ALIEN CENTER | 73 |
| 15 | ALIEN CENTER | 74 |
| 15 | ALIEN CENTER | 75 |
| 15 | ALIEN CENTER | 76 |
+---------+----------------+--------------+
11 rows in set (0.00 sec)
El NULL en inventory_id indica que no existe fila relacionada en la tabla derecha.
Outer joins de tres tablas
Puedes encadenar OUTER JOIN para preservar filas a través de varias relaciones. En este tipo de consulta, el orden importa porque define qué conjunto de filas se conserva en cada paso.
mysql> SELECT f.film_id, f.title, i.inventory_id, r.rental_date
-> FROM film f
-> LEFT OUTER JOIN inventory i
-> ON f.film_id = i.film_id
-> LEFT OUTER JOIN rental r
-> ON i.inventory_id = r.inventory_id
-> WHERE f.film_id BETWEEN 13 AND 15;
+---------+----------------+--------------+---------------------+
| film_id | title | inventory_id | rental_date |
+---------+----------------+--------------+---------------------+
| 13 | ALI FOREVER | 67 | 2005-07-31 18:11:17 |
| 13 | ALI FOREVER | 67 | 2005-08-22 21:59:29 |
| 13 | ALI FOREVER | 68 | 2005-07-28 15:26:20 |
...
| 14 | ALICE FANTASIA | NULL | NULL |
| 15 | ALIEN CENTER | 71 | 2005-05-28 02:06:37 |
...
| 15 | ALIEN CENTER | 76 | 2005-08-17 07:26:47 |
+---------+----------------+--------------+---------------------+
32 rows in set (0.01 sec)
Tip
Aunque existe RIGHT OUTER JOIN, en muchos equipos se prefiere estandarizar en LEFT OUTER JOIN. Mantiene la lectura de izquierda a derecha y reduce cambios mentales al revisar consultas complejas.
Cross joins
Un CROSS JOIN produce un producto cartesiano: combina cada fila de la primera tabla con cada fila de la segunda. Es útil para generar matrices de combinaciones, calendarios, catálogos base o escenarios de prueba, pero puede crecer muy rápido.
mysql> SELECT c.name category_name, l.name language_name
-> FROM category c
-> CROSS JOIN language l;
+---------------+---------------+
| category_name | language_name |
+---------------+---------------+
| Action | English |
| Action | Italian |
| Action | Japanese |
...
| Travel | English |
| Travel | German |
+---------------+---------------+
96 rows in set (0.00 sec)
Si category tiene 16 filas y language tiene 6, el resultado tendrá 96 filas. Esa multiplicación es exactamente el comportamiento esperado.
Natural joins
NATURAL JOIN deja que el motor determine las columnas de unión usando columnas con el mismo nombre en ambas tablas. Es importante conocerlo, pero no suele ser buena práctica en consultas de producción porque oculta la condición real del JOIN.
El siguiente ejemplo no retorna filas:
mysql> SELECT c.first_name, c.last_name, date(r.rental_date)
-> FROM customer c
-> NATURAL JOIN rental r;
Empty set (0.04 sec)
El problema es que customer y rental pueden compartir más columnas de las que realmente queremos usar como condición natural. Al proyectar únicamente las columnas necesarias desde customer, el NATURAL JOIN puede comportarse como esperamos:
mysql> SELECT cust.first_name, cust.last_name, date(r.rental_date)
-> FROM
-> (SELECT customer_id, first_name, last_name
-> FROM customer
-> ) cust
-> NATURAL JOIN rental r;
+------------+-----------+---------------------+
| first_name | last_name | date(r.rental_date) |
+------------+-----------+---------------------+
| MARY | SMITH | 2005-05-25 |
| MARY | SMITH | 2005-05-28 |
| MARY | SMITH | 2005-06-15 |
...
| AUSTIN | CINTRON | 2005-08-23 |
+------------+-----------+---------------------+
16044 rows in set (0.03 sec)
Warning
Prefiere JOIN ... ON ... cuando la consulta debe ser mantenible. La condición explícita documenta la intención y evita cambios inesperados si el esquema gana nuevas columnas con nombres compartidos.
Conclusión
Las subqueries en SQL te permiten expresar reglas de negocio que dependen de resultados intermedios sin salir del motor de base de datos. Las subconsultas no correlacionadas son ideales para filtros basados en conjuntos estáticos; las correlacionadas son útiles cuando cada fila externa necesita una evaluación específica; y EXISTS comunica con claridad validaciones de existencia.
En el lado de los JOIN, LEFT OUTER JOIN conserva filas sin coincidencias, CROSS JOIN genera combinaciones cartesianas y NATURAL JOIN delega la condición al motor, aunque conviene usarlo con cuidado. La clave es elegir la herramienta que haga más explícita la intención de la consulta.
En el siguiente artículo de la serie vamos a trabajar con Vistas (Views), una forma práctica de encapsular consultas y exponer modelos de lectura reutilizables dentro de la base de datos.


