Curso de MySQL

Subconsultas en MySQL: consultas dentro de consultas

Por Víctor Peña · Publicado el

Subconsulta en MySQL

Hola, ¿cómo están? Continuando con el curso de MySQL, hoy veremos las subconsultas: consultas dentro de otras consultas.

Permiten resolver en una sola sentencia cosas que de otro modo requerirían varios pasos.

¡Empecemos!

Qué es una subconsulta

Es un SELECT escrito dentro de otra sentencia, entre paréntesis.

SELECT nombre, apellido
FROM doctores
WHERE especialidad_id = (
    SELECT id FROM especialidades WHERE nombre = 'Pediatría'
);

La consulta interna se resuelve primero y su resultado alimenta a la externa. En este caso: busca el id de pediatría, y con ese id filtra los doctores.

Sin subconsulta tendrías que hacerlo en dos pasos: consultar el id, anotarlo, y escribir una segunda consulta.

Subconsulta que devuelve un solo valor

Cuando el resultado es un valor único, se compara con =, >, <:

-- Citas con costo mayor al promedio
SELECT fecha, costo
FROM citas
WHERE costo > (SELECT AVG(costo) FROM citas);

Un aviso: si la subconsulta devuelve más de una fila, la consulta falla con «Subquery returns more than 1 row». Si eso puede pasar, usa IN en lugar de =.

Subconsulta con IN

Cuando devuelve varios valores:

SELECT fecha, hora, costo
FROM citas
WHERE doctor_id IN (
    SELECT id FROM doctores WHERE especialidad_id = 2
);

Y su negación:

-- Pacientes que nunca han tenido una cita
SELECT nombre, apellido
FROM pacientes
WHERE id NOT IN (SELECT DISTINCT paciente_id FROM citas);

Cuidado con NOT IN y los nulos. Como vimos en la lección de operadores, si la subconsulta devuelve algún NULL, el resultado será vacío. Protégete:

WHERE id NOT IN (
    SELECT paciente_id FROM citas WHERE paciente_id IS NOT NULL
)

EXISTS: comprobar si hay coincidencias

EXISTS devuelve verdadero si la subconsulta produce al menos una fila:

-- Doctores que tienen al menos una cita
SELECT nombre, apellido
FROM doctores d
WHERE EXISTS (
    SELECT 1 FROM citas c WHERE c.doctor_id = d.id
);

Fíjate en el SELECT 1: no importa qué devuelva, solo si devuelve algo. Es la convención habitual.

Y NOT EXISTS para lo contrario:

-- Doctores sin ninguna cita
SELECT nombre, apellido
FROM doctores d
WHERE NOT EXISTS (
    SELECT 1 FROM citas c WHERE c.doctor_id = d.id
);

NOT EXISTS es más seguro que NOT IN porque no tiene el problema de los nulos. Cuando dudes, usa EXISTS.

Subconsultas correlacionadas

Las anteriores se ejecutaban una vez. Una correlacionada hace referencia a la consulta externa, y se ejecuta una vez por cada fila:

SELECT
    d.nombre,
    d.apellido,
    (SELECT COUNT(*) FROM citas c WHERE c.doctor_id = d.id) AS total_citas
FROM doctores d;

Ese c.doctor_id = d.id conecta la interna con la externa.

Son potentes pero costosas. Con 1.000 doctores, esa subconsulta se ejecuta 1.000 veces. Es el equivalente en SQL del problema N+1.

La alternativa con JOIN y GROUP BY suele ser mucho más rápida:

SELECT
    d.nombre,
    d.apellido,
    COUNT(c.id) AS total_citas
FROM doctores d
LEFT JOIN citas c ON d.id = c.doctor_id
GROUP BY d.id, d.nombre, d.apellido;

Subconsulta en el SELECT

Devuelve un valor calculado como columna:

SELECT
    fecha,
    costo,
    (SELECT AVG(costo) FROM citas) AS promedio_general,
    costo - (SELECT AVG(costo) FROM citas) AS diferencia
FROM citas;

Debe devolver un solo valor, o falla.

Subconsulta en el FROM

Se conoce como tabla derivada: el resultado de una consulta se usa como si fuera una tabla.

SELECT
    especialidad,
    total_citas
FROM (
    SELECT
        e.nombre AS especialidad,
        COUNT(c.id) AS total_citas
    FROM citas c
    JOIN doctores d ON c.doctor_id = d.id
    JOIN especialidades e ON d.especialidad_id = e.id
    GROUP BY e.id, e.nombre
) AS resumen
WHERE total_citas > 1
ORDER BY total_citas DESC;

El alias es obligatorio —aquí AS resumen—: MySQL exige nombrar la tabla derivada.

Esto resuelve un problema real: filtrar por una columna calculada. Recuerda que no puedes usar un alias del SELECT en el WHERE; con una tabla derivada, sí.

Subconsultas en INSERT, UPDATE y DELETE

No son solo para consultar:

-- Actualizar según una subconsulta
UPDATE citas
SET costo = costo * 1.10
WHERE doctor_id IN (
    SELECT id FROM doctores WHERE especialidad_id = 1
);

-- Insertar el resultado de una consulta
INSERT INTO citas_archivo
SELECT * FROM citas WHERE fecha < '2025-01-01';

-- Borrar según una subconsulta
DELETE FROM citas
WHERE paciente_id IN (
    SELECT id FROM pacientes WHERE eliminado_en IS NOT NULL
);

Una limitación de MySQL: no puedes usar en la subconsulta la misma tabla que estás modificando. La solución es envolverla en otra:

DELETE FROM citas
WHERE id IN (
    SELECT id FROM (
        SELECT id FROM citas WHERE estado = 'cancelada' LIMIT 100
    ) AS temporal
);

Subconsulta o JOIN

La pregunta inevitable. Muchas veces ambas resuelven lo mismo:

-- Con subconsulta
SELECT * FROM citas
WHERE doctor_id IN (SELECT id FROM doctores WHERE especialidad_id = 2);

-- Con JOIN
SELECT c.* FROM citas c
JOIN doctores d ON c.doctor_id = d.id
WHERE d.especialidad_id = 2;

Criterios para elegir:

Situación Preferible
Necesitas columnas de ambas tablas JOIN
Solo filtras por la otra tabla Cualquiera
Subconsulta correlacionada JOIN + GROUP BY
Comprobar existencia EXISTS
Consulta muy anidada y difícil de leer JOIN

En general, el JOIN rinde mejor y el optimizador de MySQL lo maneja mejor. La subconsulta gana en legibilidad cuando expresa una idea simple como «los que están en esta lista».

Y para casos complejos, hay una tercera opción.

CTE: subconsultas con nombre

Desde MySQL 8 existen las expresiones de tabla común, que hacen legible lo que con subconsultas anidadas sería ilegible:

WITH ingresos_por_especialidad AS (
    SELECT
        e.id,
        e.nombre,
        SUM(c.costo) AS total
    FROM citas c
    JOIN doctores d ON c.doctor_id = d.id
    JOIN especialidades e ON d.especialidad_id = e.id
    WHERE c.estado = 'atendida'
    GROUP BY e.id, e.nombre
),
promedio AS (
    SELECT AVG(total) AS media FROM ingresos_por_especialidad
)
SELECT
    i.nombre,
    i.total,
    ROUND(p.media, 2) AS promedio
FROM ingresos_por_especialidad i
CROSS JOIN promedio p
WHERE i.total > p.media;

Se lee de arriba abajo, como pasos numerados, en lugar de descifrar paréntesis anidados. Cuando una consulta te obligue a anidar tres subconsultas, usa un CTE.

Ejercicios

  1. Pacientes con más citas que el promedio de citas por paciente.
  2. La cita más cara de cada doctor.
  3. Especialidades sin ningún doctor asignado, usando NOT EXISTS.
  4. Pacientes cuya última cita fue cancelada.

Errores comunes

  • Subconsulta que devuelve varias filas comparada con =.
  • NOT IN con nulos, que devuelve vacío.
  • Correlacionadas sobre tablas grandes, cuando un JOIN habría bastado.
  • Olvidar el alias en una tabla derivada.
  • Anidar tanto que la consulta se vuelve imposible de mantener.

Para cerrar

Las subconsultas permiten expresar en una sentencia lo que de otro modo serían varios pasos. Su punto fuerte es la legibilidad; su punto débil, el rendimiento cuando son correlacionadas.

La regla práctica: EXISTS para comprobar existencia, JOIN cuando necesites columnas de ambas tablas, y CTE cuando la cosa se complique.

En la siguiente lección veremos EXPLAIN, que te dice exactamente qué está haciendo MySQL con tu consulta.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp