Curso de SQL completo: SELECT, JOIN, agregaciones, índices y transacciones (2026)

Una subconsulta (también llamada subquery o consulta anidada) es un SELECT dentro de otro SELECT. Se puede usar en el WHERE, en el FROM o incluso en el SELECT. Permiten responder preguntas complejas en una sola instrucción SQL, sin necesidad de guardar resultados intermedios en variables o tablas temporales.

En este capítulo cubrimos los tipos más usados: subconsultas escalares, subconsultas en IN / NOT IN, EXISTS / NOT EXISTS, subconsultas correlacionadas y tablas derivadas (subquery en el FROM).

Subconsulta escalar en WHERE

-- Productos cuyo precio es superior a la media
SELECT nombre, precio
FROM productos
WHERE precio > (SELECT AVG(precio) FROM productos)
ORDER BY precio DESC;

-- La subconsulta devuelve un único valor (escalar)
-- Si devolviera más de una fila, la consulta fallaría

-- Producto más caro de cada categoría
SELECT p.nombre, p.precio, c.nombre AS categoria
FROM productos p
JOIN categorias c ON c.id = p.id_categoria
WHERE p.precio = (
    SELECT MAX(precio) FROM productos WHERE id_categoria = p.id_categoria
);

IN con subconsulta

-- Clientes que han hecho al menos un pedido
SELECT nombre, email FROM clientes
WHERE id IN (SELECT DISTINCT id_cliente FROM pedidos);

-- Clientes que han pedido productos de la categoría 'Electrónica'
SELECT DISTINCT c.nombre
FROM clientes c
WHERE c.id IN (
    SELECT p.id_cliente
    FROM pedidos p
    JOIN lineas_pedido lp ON lp.id_pedido = p.id
    JOIN productos     pr ON pr.id = lp.id_producto
    JOIN categorias    cat ON cat.id = pr.id_categoria
    WHERE cat.nombre = 'Electrónica'
);

NOT IN: cuidado con los NULL

-- Clientes que NO han hecho ningún pedido
SELECT nombre FROM clientes
WHERE id NOT IN (SELECT id_cliente FROM pedidos);

-- TRAMPA: si la subconsulta devuelve algún NULL, NOT IN devuelve vacío para todos
-- Porque: 1 NOT IN (2, 3, NULL) → 1 != 2 AND 1 != 3 AND 1 != NULL → AND NULL → NULL (falso)
-- Solución: filtrar NULLs en la subconsulta
WHERE id NOT IN (SELECT id_cliente FROM pedidos WHERE id_cliente IS NOT NULL);

-- O usar NOT EXISTS, que no tiene este problema:

EXISTS y NOT EXISTS

-- Clientes con al menos un pedido (EXISTS)
SELECT nombre FROM clientes c
WHERE EXISTS (SELECT 1 FROM pedidos WHERE id_cliente = c.id);

-- Clientes sin ningún pedido (NOT EXISTS) — equivale al LEFT JOIN IS NULL
SELECT nombre FROM clientes c
WHERE NOT EXISTS (SELECT 1 FROM pedidos WHERE id_cliente = c.id);

-- EXISTS detiene la búsqueda en cuanto encuentra la primera fila que cumple
-- la condición (es eficiente aunque la subconsulta devuelva muchas filas)

Subconsulta correlacionada

Una subconsulta correlacionada hace referencia a una columna de la consulta externa. Se ejecuta una vez por cada fila de la consulta externa:

-- Para cada cliente, obtener el total de su último pedido
SELECT
    c.nombre,
    (SELECT p.total
     FROM pedidos p
     WHERE p.id_cliente = c.id
     ORDER BY p.fecha DESC
     LIMIT 1) AS ultimo_total
FROM clientes c;

-- Si un cliente no tiene pedidos, la subconsulta devuelve NULL

Tabla derivada (subquery en FROM)

-- Calcular el precio medio por categoría y comparar cada producto con él
SELECT
    pr.nombre,
    pr.precio,
    medias.precio_medio_cat,
    ROUND(pr.precio - medias.precio_medio_cat, 2) AS diferencia
FROM productos pr
JOIN (
    SELECT id_categoria, ROUND(AVG(precio), 2) AS precio_medio_cat
    FROM productos
    GROUP BY id_categoria
) AS medias ON medias.id_categoria = pr.id_categoria
ORDER BY pr.id_categoria, diferencia DESC;

WITH (CTE): alternativa más legible a las tablas derivadas

Las Common Table Expressions (CTEs), disponibles en MySQL 8.0 y PostgreSQL, hacen más legibles las subconsultas complejas:

WITH medias_cat AS (
    SELECT id_categoria, ROUND(AVG(precio), 2) AS precio_medio
    FROM productos
    GROUP BY id_categoria
)
SELECT
    pr.nombre,
    pr.precio,
    m.precio_medio,
    ROUND(pr.precio - m.precio_medio, 2) AS diferencia
FROM productos pr
JOIN medias_cat m ON m.id_categoria = pr.id_categoria;

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP