La lección anterior terminó con una lista de preguntas que no sabías responder todavía, y todas tenían la misma forma: comparar cada fila con el resultado de otra consulta. ¿Qué productos superan el precio medio del catálogo? ¿Qué clientes tienen un ticket medio por encima de la media general —la pregunta que 04-06 dejó explícitamente reservada para esta lección? ¿Qué pedidos incluyen el producto más caro?

La respuesta a todas ellas es la misma herramienta: una subconsulta, una consulta escrita entre paréntesis dentro de otra. Aquí aprenderás el vocabulario, los tres tipos de subconsulta según lo que devuelven, cómo se usan con IN, ANY y ALL, y los tres errores clásicos que producen desde un mensaje rojo hasta —mucho peor— un resultado vacío sin ningún aviso.

Contenido

  1. Qué es una subconsulta: vocabulario, correlación y tipos
  2. Subconsulta escalar en el WHERE
  3. La pregunta pendiente de 04-06: el ticket medio
  4. Subconsultas de lista: IN, ANY/SOME y ALL
  5. Los tres errores clásicos
  6. Dónde puede aparecer una subconsulta
  7. Errores Comunes y Consejos
  8. Ejercicios
  9. Conclusión

  1. Qué es una subconsulta y cómo se llama cada parte

Una subconsulta (o consulta anidada) es una sentencia SELECT completa, escrita entre paréntesis, que aparece dentro de otra sentencia SQL. El motor la ejecuta y usa su resultado como si fuera un valor, una lista o una tabla.

SELECT id, nombre, precio
FROM productos
WHERE precio > (SELECT AVG(precio) FROM productos);
--              └──────── subconsulta ────────┘

La consulta que contiene a la otra es la consulta externa (outer query); la de dentro, la subconsulta (subquery). Una subconsulta puede contener otra, sin más límite que la legibilidad. Tres reglas de sintaxis sin excepción: los paréntesis son obligatorios; la subconsulta se escribe entera (SELECT, FROM, WHERE, GROUP BY… lo que necesite); y un ORDER BY dentro de una subconsulta casi nunca sirve de nada, salvo acompañado de un LIMIT.

No correlacionada frente a correlacionada

Esta es la distinción que estructura el módulo entero, y conviene fijarla antes que ninguna otra.

No correlacionada Correlacionada
Referencia a la consulta externa No , usa un alias de fuera
¿Se puede ejecutar sola? , copiando y pegando No: da error
Cuántas veces se evalúa Una para toda la consulta Una por cada fila candidata
Coste conceptual Constante Proporcional al número de filas
Lección Esta (07-01) La siguiente (07-02)
-- NO CORRELACIONADA: la subconsulta no menciona nada de fuera
SELECT id, nombre FROM productos AS p
WHERE p.precio > (SELECT AVG(precio) FROM productos);

-- CORRELACIONADA: la subconsulta usa p.categoria_id, que viene de fuera
SELECT id, nombre FROM productos AS p
WHERE p.precio > (SELECT AVG(precio) FROM productos WHERE categoria_id = p.categoria_id);

La prueba práctica es infalible: selecciona la subconsulta, ejecútala sola y mira qué pasa. La primera devuelve 9.0350000000000000. La segunda da ERROR: missing FROM-clause entry for table "p", porque p no existe fuera de la consulta externa. Toda esta lección trata de las no correlacionadas.

Los tres tipos según lo que devuelven

La segunda clasificación, transversal a la anterior, se refiere a la forma del resultado, y determina dónde puede aparecer la subconsulta y con qué operadores se combina.

Tipo Devuelve Ejemplo Se usa con
Escalar Una fila y una columna: un valor (SELECT AVG(precio) FROM productos) =, >, <, >=, <=, <>; o como columna del SELECT
De fila Una fila con varias columnas (SELECT MAX(precio), MIN(precio) FROM productos) Comparación de tuplas: (a, b) = (SELECT ...)
De tabla (multifila) Varias filas (SELECT cliente_id FROM pedidos) IN, NOT IN, ANY, ALL, EXISTS, o en el FROM

Las escalares y las de tabla cubren el 99 % del SQL que escribirás. Las de fila son elegantes pero raras: WHERE (precio, stock) = (SELECT MAX(precio), 40 FROM productos) devuelve una única fila, el matcha (22,00 € y 40 unidades).

Nota de dialecto: los constructores de fila (a, b) = (...) funcionan en PostgreSQL, MySQL y MariaDB; SQLite y SQL Server no los admiten y exigen dos condiciones unidas con AND.

  1. Subconsulta escalar en el WHERE

El caso de uso más común de todos: comparar cada fila con un valor calculado sobre el conjunto. La pregunta es "¿qué productos están por encima del precio medio del catálogo?", y el intento ingenuo es este:

-- ⚠️ INCORRECTA
SELECT id, nombre, precio FROM productos WHERE precio > AVG(precio);
ERROR:  aggregate functions are not allowed in WHERE
LINE 1: ... nombre, precio FROM productos WHERE precio > AVG(precio);
                                                         ^

Es el mismo error de 04-06 y por la misma razón: cuando se ejecuta el WHERE (paso 2 del orden lógico) el motor mira una fila cada vez y no ha calculado ningún agregado. La subconsulta lo resuelve porque es otra consulta, con su propio recorrido completo por la tabla:

-- ✅ CORRECTA
SELECT p.id, p.nombre AS producto, cat.nombre AS categoria, p.precio
FROM productos  AS p
JOIN categorias AS cat ON p.categoria_id = cat.id
WHERE p.precio > (SELECT AVG(precio) FROM productos)
ORDER BY p.precio DESC;
id producto categoria precio
15 Té verde matcha ceremonial 30 g Bebidas 22.00
6 Crema facial de aloe vera 50 ml Cosmética natural 18.90
20 Cápsulas de espirulina 120 uds Complementos 16.40
8 Aceite corporal de almendras 200 ml Cosmética natural 14.25
13 Velas de cera de soja (pack 2) Hogar sostenible 13.75
1 Aceite de oliva virgen extra 500 ml Alimentación 12.50
10 Detergente ecológico concentrado 1 L Hogar sostenible 11.20
12 Bolsas reutilizables de algodón (pack 5) Hogar sostenible 9.90
3 Miel de azahar cruda 500 g Alimentación 9.75

9 filas: 9 productos de 20 superan los 9,035 € de precio medio (SELECT ROUND(AVG(precio), 4) FROM productos9.0350, la cifra de 04-04). Lo importante es cómo se ejecuta: PostgreSQL evalúa la subconsulta una sola vez, obtiene 9.0350000000000000 y sustituye la expresión por ese número; a partir de ahí la consulta externa es un WHERE precio > 9.0350000000000000 corriente. No hay bucle: es una constante calculada al vuelo. Y ojo con un detalle que importa: la comparación se hace con el valor sin redondear — con otros datos, un céntimo decide si una fila entra o sale. Nunca redondees el valor de comparación; redondea solo lo que muestras.

  1. La pregunta pendiente de 04-06: el ticket medio

En 04-06 calculaste el ticket medio de cada cliente y comprobaste que la media global de los 20 tickets es de 36,40 €, pero no pudiste unir las dos cosas: HAVING sabe comparar el agregado de un grupo con una constante o con otro agregado del mismo grupo, nunca con un agregado calculado sobre otro conjunto de filas. Una subconsulta escalar en el HAVING es la pieza que faltaba.

Primero, el valor de referencia. La media global no es la media de las 47 líneas (eso daría los 15,49 € de importe medio de línea), sino la media de los 20 tickets:

SELECT COUNT(DISTINCT pedido_id) AS pedidos,
       ROUND(SUM(cantidad * precio_unitario * (1 - descuento)), 2) AS facturacion,
       ROUND(SUM(cantidad * precio_unitario * (1 - descuento))
             / COUNT(DISTINCT pedido_id), 2) AS ticket_medio_global
FROM lineas_pedido;
pedidos facturacion ticket_medio_global
20 727.95 36.40

Y ahora la misma expresión, inyectada en el HAVING:

-- ✅ La consulta que 04-06 dejó pendiente
SELECT c.id,
       c.nombre || ' ' || c.apellidos AS cliente,
       c.pais,
       COUNT(DISTINCT pe.id) AS pedidos,
       ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2) AS total,
       ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento))
             / COUNT(DISTINCT pe.id), 2) AS ticket_medio
FROM lineas_pedido AS lp
JOIN pedidos  AS pe ON lp.pedido_id  = pe.id
JOIN clientes AS c  ON pe.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellidos, c.pais
HAVING SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento))
       / COUNT(DISTINCT pe.id)
       > (SELECT SUM(cantidad * precio_unitario * (1 - descuento))
                 / COUNT(DISTINCT pedido_id)
          FROM lineas_pedido)
ORDER BY ticket_medio DESC;
id cliente pais pedidos total ticket_medio
10 Julien Moreau Francia 1 66.90 66.90
7 Sofia Moreira Costa Portugal 2 111.88 55.94
8 Tiago Almeida Nunes Portugal 1 44.60 44.60

Tres clientes de doce, exactamente los que 04-06 anticipó. Paso a paso:

  1. FROM + dos JOIN: se parte de las 47 líneas de detalle y se sube hasta el cliente; es la consulta canónica de detalle del curso. El GROUP BY forma 12 grupos, uno por cliente comprador (los clientes 13, 14 y 15 ya los descartó el INNER JOIN).
  2. La subconsulta escalar se evalúa una vez y devuelve 36.39725, la media exacta sin redondear.
  3. HAVING compara el ticket medio de cada grupo con esa constante y descarta nueve grupos.
  4. SELECT proyecta y redondea. El redondeo es solo de presentación: la comparación ya se hizo con toda la precisión.

Y una lectura de negocio: los tres son clientes extranjeros, ninguno español supera la media (el más cercano es Lucía, con 35,87 €). Los portes a Portugal y Francia cuestan 9,90 € y 12,50 € frente a los 4,95 € nacionales, y el cliente compensa haciendo pedidos más grandes. Eso sí, dos de los tres tienen un solo pedido: el caso realmente sólido es Sofia, con dos tickets de 64,88 € y 47,00 €.

Por qué va en el HAVING y no en el WHERE: la condición compara un agregado del grupo con la constante, y el WHERE no puede usar agregados. La subconsulta solo aporta el número con el que comparar.

  1. Subconsultas de lista: IN, ANY/SOME y ALL

Cuando la subconsulta devuelve varias filas de una sola columna, la forma natural de usarla es IN. Ya conoces el operador de 04-02 con una lista escrita a mano; ahora la lista la calcula otra consulta.

-- ✅ Clientes que han hecho algún pedido
SELECT c.id, c.nombre, c.apellidos, c.ciudad, c.pais
FROM clientes AS c
WHERE c.id IN (SELECT cliente_id FROM pedidos)
ORDER BY c.id LIMIT 3;
id nombre apellidos ciudad pais
1 Lucía Martínez Soler Valencia España
2 Carlos Ferrer Ibáñez Valencia España
3 Marta Sanchis Gil Castellón España

(3 primeras de 12 filas: los clientes 1 a 12, los 12 compradores.) Aquí hay un detalle capital que retomaremos en 07-05: la subconsulta devuelve 20 valores (uno por pedido, con repeticiones), pero el resultado tiene 12 filas. IN no multiplica: pregunta si el valor está en la lista y responde sí o no una sola vez por fila externa. Un INNER JOIN con pedidos habría devuelto 20 filas.

Un segundo ejemplo, con una subconsulta agregada: productos de las categorías que tienen más de tres productos.

SELECT COUNT(*) AS productos
FROM productos AS p
WHERE p.categoria_id IN (SELECT categoria_id FROM productos
                         GROUP BY categoria_id HAVING COUNT(*) > 3);
productos
17

17 productos de 20. Las categorías con más de tres referencias son Alimentación (5), Cosmética natural (4), Hogar sostenible (4) y Bebidas (4); quedan fuera Higiene personal (productos 18 y 19) y Complementos (el 20). La subconsulta es una consulta agregada completa, con su GROUP BY y su HAVING: puedes calcular un conjunto de claves con toda la maquinaria del módulo 4 y usarlo como filtro.

La equivalencia con = ANY. El estándar define IN como azúcar sintáctico de = ANY: c.id IN (sub) y c.id = ANY (sub) producen el mismo plan y el mismo resultado. IN se lee mejor y es lo que verás en el código real; = ANY importa porque explica la familia entera:

Escritura Verdadero cuando… Equivale a
x = ANY (sub) x coincide con al menos uno x IN (sub)
x <> ALL (sub) x es distinto de todos x NOT IN (sub)
x > ANY (sub) x supera al menos uno: supera al mínimo x > (SELECT MIN(...) ...)
x > ALL (sub) x los supera todos: supera al máximo x > (SELECT MAX(...) ...)
x < ANY (sub) x es menor que el máximo x < (SELECT MAX(...) ...)
x < ALL (sub) x es menor que el mínimo x < (SELECT MIN(...) ...)

SOME es un sinónimo exacto de ANY que nadie usa. La forma que de verdad aparece es > ALL, y su pregunta natural es "más caro que cualquiera de los de aquella categoría":

SELECT p.id, p.nombre AS producto, p.precio
FROM productos AS p
WHERE p.precio > ALL (SELECT precio FROM productos WHERE categoria_id = 1)
ORDER BY p.precio DESC;
id producto precio
15 Té verde matcha ceremonial 30 g 22.00
6 Crema facial de aloe vera 50 ml 18.90
20 Cápsulas de espirulina 120 uds 16.40
8 Aceite corporal de almendras 200 ml 14.25
13 Velas de cera de soja (pack 2) 13.75

5 productos cuestan más que todos los de Alimentación, cuyo tope es el aceite de oliva (12,50 €). La escritura > (SELECT MAX(precio) FROM productos WHERE categoria_id = 1) devuelve lo mismo y se lee mejor. Con ANY en lugar de ALL la condición sería "más caro que el más barato de Alimentación" (1,95 €) y saldrían 19 productos.

Cuidado con los conjuntos vacíos. Si la subconsulta no devuelve ninguna fila, > ALL es verdadero para todas (no hay contraejemplo) y > ANY es falso para todas (no hay caso favorable). Impecable en lógica y desconcertante en la práctica: precio > ALL (SELECT precio FROM productos WHERE categoria_id = 99) devuelve los 20 productos.

  1. Los tres errores clásicos

5.1. La escalar que devuelve más de una fila

-- ⚠️ INCORRECTA
SELECT id, nombre, precio
FROM productos
WHERE precio > (SELECT precio FROM productos WHERE categoria_id = 1);
ERROR:  more than one row returned by a subquery used as an expression

Alimentación tiene cinco productos, la subconsulta devuelve cinco precios y > no sabe con cuál comparar. Los arreglos son tres y cada uno responde a una pregunta distinta: > (SELECT MAX(precio) ...) y > ALL (...) dicen "más caro que el más caro"; > ANY (...) dice "más caro que alguno". Es el menos peligroso de los tres porque es ruidoso, y es un error de datos, no de sintaxis: la misma consulta funcionaría si la categoría tuviera un solo producto y reventaría al dar de alta el segundo. Si la subconsulta devuelve varias columnas, el mensaje es ERROR: subquery must return only one column.

5.2. La escalar que devuelve cero filas — el peligroso

-- ⚠️ Devuelve 0 filas, y no hay ningún error
SELECT id, nombre, precio
FROM productos
WHERE precio > (SELECT AVG(precio) FROM productos WHERE categoria_id = 99);
(0 filas)

Ningún mensaje, ninguna advertencia. La categoría 99 no existe, la subconsulta no encuentra filas, AVG sobre un conjunto vacío devuelve NULL (04-04) y precio > NULL se evalúa a UNKNOWN para las veinte filas. Como el WHERE solo deja pasar lo que es TRUE (04-03), el resultado se vacía en silencio. Este es el más peligroso de los tres, porque un informe vacío parece un informe legítimo: "este mes no hubo ninguno". Cómo defenderse: ejecuta siempre la subconsulta sola antes de anidarla; envuélvela en COALESCE cuando exista un valor por defecto sensato (> COALESCE((SELECT AVG(...)...), 0), 06-04); y si la pregunta es de existencia, usa EXISTS, que nunca devuelve NULL (07-03).

5.3. NOT IN con NULL — el reencuentro con 04-02

En 04-02 lo llamaste "el error más caro de SQL". Con subconsultas es mucho más fácil de cometer, porque ya no ves la lista: la calcula otra consulta y no sabes si trae nulos.

-- ⚠️ INCORRECTA: devuelve 0 filas
SELECT id, nombre, apellidos, puesto
FROM empleados
WHERE id NOT IN (SELECT empleado_id FROM pedidos);
(0 filas)

Y sin embargo sabes que hay cinco empleados sin pedidos: solo el 4, el 5 y el 6 aparecen en pedidos. Lo que ha pasado es que la subconsulta devuelve {4, 5, 6, NULL} — los 10 pedidos web tienen empleado_id a NULL. Para el empleado 1:

1 NOT IN (4, 5, 6, NULL)
≡ 1 <> 4 AND 1 <> 5 AND 1 <> 6 AND 1 <> NULL
≡ TRUE    AND TRUE    AND TRUE    AND UNKNOWN
≡ UNKNOWN     →  no es TRUE  →  la fila se descarta

Lo mismo para las ocho filas. Con un solo NULL en la lista, NOT IN no puede devolver TRUE jamás. Los tres arreglos —filtrar los nulos dentro, NOT EXISTS (07-03) y el anti-join de 03-03— son estos:

WHERE id NOT IN (SELECT empleado_id FROM pedidos WHERE empleado_id IS NOT NULL)  -- ✅ 1
WHERE NOT EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.empleado_id = empleados.id) -- ✅ 2
FROM empleados AS e LEFT JOIN pedidos AS pe ON pe.empleado_id = e.id
WHERE pe.id IS NULL                                                              -- ✅ 3

Los tres devuelven lo mismo:

id nombre apellidos puesto
1 Rosa Alcázar Vives Directora general
2 Andrés Company Talens Responsable de ventas
3 Beatriz Nadal Ripoll Responsable de logística
7 Irene Salvador Mira Operaria de almacén
8 Daniel Vercher Lluch Analista de datos

5 filas. Y hay una asimetría que sorprende a todo el mundo: IN sí funciona con nulos. 4 IN (4, 5, 6, NULL) es TRUE, porque basta con que una comparación acierte. El problema es exclusivo de la negación. La comparación de las cuatro formas de responder "qué no casa" llega en 07-03, y su tabla definitiva, en 07-05.

  1. Dónde puede aparecer una subconsulta

Casi en cualquier sitio donde quepa un valor o una tabla:

Lugar Qué tipo Para qué Lección
SELECT Escalar Columna calculada a partir de otra tabla 07-04
FROM / JOIN ... ON De tabla Tabla derivada: agregar y volver a agregar, o unir contra un agregado 07-04
WHERE Cualquiera Filtrar con un valor o una lista calculados Esta
HAVING Escalar Comparar el agregado de un grupo con uno global Esta, sección 3
INSERT ... SELECT De tabla Insertar el resultado de una consulta 05-02
UPDATE ... SET Escalar Calcular el nuevo valor desde otra tabla 05-03
UPDATE/DELETE ... WHERE Cualquiera Elegir qué filas se modifican o se borran 05-03, 05-04

Dos ejemplos del módulo 5 revisitados, ahora que sabes cómo se llaman: UPDATE productos SET precio = ROUND(precio * 1.05, 2) WHERE id NOT IN (SELECT producto_id FROM lineas_pedido) sube un 5 % lo que nunca se ha vendido, y DELETE FROM resenas WHERE cliente_id NOT IN (SELECT id FROM clientes) limpia reseñas huérfanas. Los dos son seguros porque lineas_pedido.producto_id y clientes.id son NOT NULL. Si alguna admitiera nulos, el UPDATE no modificaría nada y el DELETE no borraría nada — la trampa de 5.3, ahora con consecuencias sobre los datos. Y dónde no puede aparecer una subconsulta: en el GROUP BY, ni —en PostgreSQL— dentro de una restricción CHECK.

Errores Comunes y Consejos

  • Escribir un agregado en el WHERE. aggregate functions are not allowed in WHERE. Lo que necesitas es una subconsulta escalar.
  • Usar una subconsulta multifila donde se espera un valor. more than one row returned by a subquery used as an expression. Añade un agregado, un ORDER BY ... LIMIT 1, o cambia a IN/ANY/ALL.
  • No comprobar que la escalar devuelve algo. Si devuelve cero filas vale NULL, el filtro se vacía en silencio y el informe parece correcto. Es el error más caro de la lección.
  • NOT IN sobre una columna que admite nulos. Cero filas, siempre. Usa NOT EXISTS o filtra los nulos dentro de la subconsulta.
  • Redondear el valor de comparación. > ROUND(AVG(precio), 2) no es > AVG(precio). Redondea al mostrar, no al comparar. Y evita el ORDER BY dentro de una subconsulta de lista: no aporta nada y cuesta tiempo.
  • Consejo: ejecuta siempre la subconsulta sola primero. Es la técnica de depuración número uno del módulo; si no se puede ejecutar sola, es correlacionada.
  • Consejo: si necesitas columnas de la otra tabla en el resultado, no es una subconsulta: es un JOIN. La subconsulta filtra y calcula; no aporta columnas al SELECT externo (07-05).

Ejercicios

Ejercicio 1

Marketing quiere una lista de productos caros medidos con una vara concreta: los que superan la media de precios de la categoría Cosmética natural. Muestra id, nombre, categoría y precio, ordenados por precio descendente, y no escribas el umbral a mano. Después responde: ¿cuántos de los productos que salen son de Cosmética natural, y por qué los otros dos de esa categoría no aparecen?

Ejercicio 2

Un compañero quiere saber qué clientes nunca han escrito una reseña y ha escrito esto:

-- ⚠️ Sospechosa
SELECT id, nombre, apellidos FROM clientes
WHERE id NOT IN (SELECT cliente_id FROM resenas);
  1. ¿Funciona? Justifica la respuesta mirando el esquema de 01-06, sin ejecutarla.
  2. Da el resultado y di cuántos de esos clientes sí han comprado alguna vez.
  3. Reescríbela con NOT EXISTS y con un anti-join, y explica por qué aquí las tres son equivalentes.

Ejercicio 3

Dirección pregunta: "¿qué pedidos incluyen el producto más caro del catálogo?". Escribe una única consulta que devuelva id del pedido, fecha, cliente y estado, sin escribir a mano ni el nombre ni el id de ese producto. (Pista: necesitarás dos subconsultas, una dentro de otra.) Indica después qué pasaría si hubiera dos productos empatados en el precio máximo.

Soluciones

Solución 1

SELECT p.id, p.nombre AS producto, cat.nombre AS categoria, p.precio
FROM productos  AS p
JOIN categorias AS cat ON p.categoria_id = cat.id
WHERE p.precio > (SELECT AVG(precio) FROM productos WHERE categoria_id = 2)
ORDER BY p.precio DESC;
id producto categoria precio
15 Té verde matcha ceremonial 30 g Bebidas 22.00
6 Crema facial de aloe vera 50 ml Cosmética natural 18.90
20 Cápsulas de espirulina 120 uds Complementos 16.40
8 Aceite corporal de almendras 200 ml Cosmética natural 14.25
13 Velas de cera de soja (pack 2) Hogar sostenible 13.75
1 Aceite de oliva virgen extra 500 ml Alimentación 12.50

6 filas. El umbral calculado es 11,5375 € (46,15 € entre 4 productos). Solo dos son de Cosmética natural, la crema de aloe y el aceite corporal; los otros dos de la categoría —champú 8,40 € y bálsamo 4,60 €— quedan por debajo de su propia media, que está tirada hacia arriba precisamente por los dos caros.

El punto de fondo: el umbral sale de una categoría pero se aplica a todo el catálogo, porque una subconsulta no correlacionada se evalúa una vez y vale igual para las veinte filas. Comparar cada producto con la media de su propia categoría exige una correlacionada: lección siguiente.

Solución 2

1. Sí funciona, y se sabe sin ejecutarla: resenas.cliente_id está declarada NOT NULL (01-06, tabla 3.8), así que la subconsulta no puede devolver ningún NULL y NOT IN se comporta correctamente. La sospecha era el reflejo correcto ante cualquier NOT IN; el esquema la despeja. 2. El resultado:

id nombre apellidos
5 Ana Belmonte Roca
10 Julien Moreau
12 Diego Ramos Herrera
13 Núria Bosch Ferrer
14 Hugo Iglesias Pardo
15 Inés Carrasco Vega

6 clientes de 15, y tres de ellos sí han comprado: Ana (2 pedidos), Julien (1) y Diego (1). Los otros tres son los conocidos 13, 14 y 15, que no han comprado nunca y por tanto no podían reseñar nada. Distinguir ambos grupos importa: a Ana, Julien y Diego se les puede pedir una opinión; a Núria, Hugo e Inés hay que venderles algo primero.

3. Las dos reescrituras, con el mismo resultado de seis filas:

SELECT c.id, c.nombre, c.apellidos FROM clientes AS c          -- NOT EXISTS (07-03)
WHERE NOT EXISTS (SELECT 1 FROM resenas AS r WHERE r.cliente_id = c.id) ORDER BY c.id;

SELECT c.id, c.nombre, c.apellidos FROM clientes AS c          -- anti-join (03-03)
LEFT JOIN resenas AS r ON r.cliente_id = c.id WHERE r.id IS NULL ORDER BY c.id;

Las tres son equivalentes aquí por una única razón: resenas.cliente_id es NOT NULL. Si mañana se permitieran reseñas anónimas con cliente_id nulo, la versión con NOT IN pasaría a devolver cero filas y las otras dos seguirían funcionando. La equivalencia depende del esquema, no de la sintaxis.

Solución 3

SELECT pe.id AS pedido_id,
       pe.fecha_pedido,
       c.nombre || ' ' || c.apellidos AS cliente,
       pe.estado
FROM pedidos  AS pe
JOIN clientes AS c ON pe.cliente_id = c.id
WHERE pe.id IN (SELECT lp.pedido_id FROM lineas_pedido AS lp
                WHERE lp.producto_id = (SELECT id FROM productos
                                        ORDER BY precio DESC LIMIT 1))
ORDER BY pe.id;
pedido_id fecha_pedido cliente estado
4 2025-04-19 Javier Ortega Ruiz entregado
12 2025-10-01 Julien Moreau entregado
17 2026-01-13 Sofia Moreira Costa enviado

3 pedidos. El producto más caro es el Té verde matcha ceremonial 30 g (22,00 €) y se ha vendido tres veces. Hay dos niveles: la subconsulta interna devuelve un valor escalar (el id del producto más caro) y la intermedia, una lista de pedido_id.

Si hubiera empate, la interna seguiría devolviendo una sola fila —LIMIT 1 corta arbitrariamente— y perderías en silencio los pedidos del otro producto. La versión robusta cambia el = por un IN y elimina el LIMIT: WHERE lp.producto_id IN (SELECT id FROM productos WHERE precio = (SELECT MAX(precio) FROM productos)). Con los datos actuales devuelve lo mismo, pero no se rompe el día que alguien dé de alta un segundo producto de 22,00 €. ORDER BY ... LIMIT 1 dentro de una subconsulta es cómodo y frágil.

Conclusión

Has abierto la puerta del módulo:

  • Una subconsulta es una consulta entre paréntesis dentro de otra; la que la contiene es la consulta externa. Según lo que devuelven hay tres tipos —escalar, de fila y de tabla—, y cada uno admite unos operadores distintos.
  • La distinción que organiza el módulo es no correlacionada (no menciona nada de fuera, se ejecuta sola, se evalúa una vez) frente a correlacionada (usa un alias externo, no se puede ejecutar aislada, se evalúa una vez por fila).
  • Una escalar en el WHERE resuelve "por encima de la media": 9 de los 20 productos superan los 9,035 € del catálogo. Y una escalar en el HAVING cierra la pregunta que 04-06 dejó pendiente: Julien, Sofia y Tiago son los únicos con un ticket medio superior a los 36,40 € de media global.
  • IN (SELECT ...) filtra por una lista calculada —12 clientes compradores, 17 productos de categorías con más de tres referencias— y equivale a = ANY; > ALL compara con el máximo y > ANY con el mínimo. Y conoces los tres errores clásicos: la escalar con varias filas (ruidosa), la escalar con cero filas (silenciosa: da NULL y vacía el resultado) y NOT IN con nulos (cero filas garantizadas).

Todas las subconsultas de esta lección tienen algo en común: se calculan una vez y valen lo mismo para todas las filas. Por eso ninguna ha podido responder a "qué productos superan la media de su categoría": ese umbral es distinto para cada fila. En la lección siguiente, subconsultas correlacionadas, la subconsulta empezará a mirar hacia fuera —a la fila que la consulta externa está examinando en ese momento— y se ejecutará una vez por cada una de ellas. Cambia el modelo mental, cambia el coste y aparece toda una familia de preguntas nuevas.

Curso de SQL

Módulo 1: Introducción a SQL

Módulo 2: Consultas básicas de SQL

Módulo 3: Trabajando con múltiples tablas

Módulo 4: Filtrado avanzado de datos

Módulo 5: Manipulación de datos

Módulo 6: Funciones avanzadas de SQL

Módulo 7: Subconsultas y consultas anidadas

Módulo 8: Índices y optimización de rendimiento

Módulo 9: Transacciones y concurrencia

Módulo 10: Temas avanzados

Módulo 11: SQL en la práctica

Módulo 12: Proyecto final

© Copyright 2026. Todos los derechos reservados