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
- Qué es una subconsulta: vocabulario, correlación y tipos
- Subconsulta escalar en el
WHERE - La pregunta pendiente de 04-06: el ticket medio
- Subconsultas de lista:
IN,ANY/SOMEyALL - Los tres errores clásicos
- Dónde puede aparecer una subconsulta
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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 | Sí, usa un alias de fuera |
| ¿Se puede ejecutar sola? | Sí, 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 conAND.
- Subconsulta escalar en el
WHERE
WHEREEl 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:
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 productos → 9.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.
- 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:
FROM+ dosJOIN: se parte de las 47 líneas de detalle y se sube hasta el cliente; es la consulta canónica de detalle del curso. ElGROUP BYforma 12 grupos, uno por cliente comprador (los clientes 13, 14 y 15 ya los descartó elINNER JOIN).- La subconsulta escalar se evalúa una vez y devuelve
36.39725, la media exacta sin redondear. HAVINGcompara el ticket medio de cada grupo con esa constante y descarta nueve grupos.SELECTproyecta 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
HAVINGy no en elWHERE: la condición compara un agregado del grupo con la constante, y elWHEREno puede usar agregados. La subconsulta solo aporta el número con el que comparar.
- Subconsultas de lista:
IN, ANY/SOME y ALL
IN, ANY/SOME y ALLCuando 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,
> ALLes verdadero para todas (no hay contraejemplo) y> ANYes 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.
- 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);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);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);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 -- ✅ 3Los 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.
- 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, unORDER BY ... LIMIT 1, o cambia aIN/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 INsobre una columna que admite nulos. Cero filas, siempre. UsaNOT EXISTSo 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 elORDER BYdentro 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 alSELECTexterno (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);- ¿Funciona? Justifica la respuesta mirando el esquema de 01-06, sin ejecutarla.
- Da el resultado y di cuántos de esos clientes sí han comprado alguna vez.
- Reescríbela con
NOT EXISTSy 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
WHEREresuelve "por encima de la media": 9 de los 20 productos superan los 9,035 € del catálogo. Y una escalar en elHAVINGcierra 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;> ALLcompara con el máximo y> ANYcon el mínimo. Y conoces los tres errores clásicos: la escalar con varias filas (ruidosa), la escalar con cero filas (silenciosa: daNULLy vacía el resultado) yNOT INcon 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
- ¿Qué es SQL?
- Configurando tu entorno SQL
- Sintaxis básica de SQL
- Entendiendo bases de datos y tablas
- El modelo relacional: claves primarias y foráneas
- La base de datos del curso: TiendaVerde
Módulo 2: Consultas básicas de SQL
- Instrucción SELECT
- Alias, expresiones y columnas calculadas
- Filtrando datos con WHERE
- DISTINCT y eliminación de duplicados
- Ordenando datos con ORDER BY
- Limitando resultados con LIMIT
Módulo 3: Trabajando con múltiples tablas
- Operaciones JOIN
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN y CROSS JOIN
- Uniones de conjuntos: UNION, INTERSECT y EXCEPT
Módulo 4: Filtrado avanzado de datos
- Usando LIKE para coincidencia de patrones
- Operadores IN y BETWEEN
- Valores NULL y IS NULL
- Funciones de agregación: COUNT, SUM, AVG, MIN y MAX
- Agregando datos con GROUP BY
- Cláusula HAVING
Módulo 5: Manipulación de datos
- Creando tablas y restricciones con CREATE TABLE
- Instrucción INSERT
- Instrucción UPDATE
- Instrucción DELETE
- Instrucción UPSERT (MERGE)
- Modificando el esquema: ALTER TABLE y migraciones seguras
Módulo 6: Funciones avanzadas de SQL
- Funciones de cadena
- Funciones numéricas
- Funciones de fecha y hora
- Conversión de tipos y manejo de NULL: CAST y COALESCE
- Expresiones condicionales
Módulo 7: Subconsultas y consultas anidadas
- Introducción a subconsultas
- Subconsultas correlacionadas
- EXISTS y NOT EXISTS
- Usando subconsultas en cláusulas SELECT, FROM y WHERE
- Subconsultas o JOIN: cuál elegir
Módulo 8: Índices y optimización de rendimiento
- Entendiendo los índices
- Creación y gestión de índices
- Tipos de índice y cuándo no indexar
- Técnicas de optimización de consultas
- Análisis del rendimiento de consultas
Módulo 9: Transacciones y concurrencia
- Introducción a las transacciones
- Propiedades ACID
- Instrucciones de control de transacciones
- Niveles de aislamiento y anomalías de concurrencia
- Manejo de concurrencia: bloqueos e interbloqueos
Módulo 10: Temas avanzados
- Vistas
- Expresiones de tabla comunes (CTE)
- Funciones de ventana
- Procedimientos almacenados
- Triggers
- JSON y datos semiestructurados
Módulo 11: SQL en la práctica
- Casos de uso en el mundo real
- Mejores prácticas
- Seguridad: inyección SQL, permisos y roles
- SQL para análisis de datos
- SQL en desarrollo web
