Todas las subconsultas de 07-01 tenían algo en común: se calculaban una vez y valían lo mismo para todas las filas. Por eso ninguna pudo responder a la pregunta que quedó abierta al final: ¿qué productos superan la media de su categoría? Ese umbral no es uno, son seis —6,18 € para Alimentación, 11,54 € para Cosmética natural, 8,90 € para Bebidas…— y cada fila necesita el suyo.
Una subconsulta correlacionada hace exactamente eso: mira hacia fuera, hacia la fila que la consulta externa está examinando en ese instante, y se recalcula para cada una. Cambia el modelo mental (deja de ser una constante y pasa a ser un bucle), cambia el coste y se abre una familia entera de preguntas: el último pedido de cada cliente, el importe de su pedido más caro, la reseña más reciente de cada producto.
Contenido
- Cómo se reconoce una correlacionada
- El modelo mental: una ejecución por fila
- El caso canónico: cada producto frente a la media de su categoría
- La variante: el más caro de su categoría, y la autocorrelación
- Cuatro casos más de TiendaVerde
- El alcance de los alias: quién ve a quién
- El coste: N ejecuciones y qué hace el planificador
- Cuándo usarla y cuándo delata que falta otra cosa
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Cómo se reconoce una correlacionada
La señal es una sola: dentro de la subconsulta aparece un alias que pertenece a la consulta externa.
-- No correlacionada: todo lo que menciona está en su propio FROM
SELECT AVG(precio) FROM productos;
-- Correlacionada: p.categoria_id no está en su FROM, viene de fuera
SELECT AVG(precio) FROM productos WHERE categoria_id = p.categoria_id;Copia esa segunda consulta en psql y ejecútala sola:
ERROR: missing FROM-clause entry for table "p"
LINE 1: ...T AVG(precio) FROM productos WHERE categoria_id = p.categori...
^Ese error es el diagnóstico, no un problema: te confirma que la subconsulta depende de su entorno y que solo tiene sentido dentro de la consulta que la contiene. Es la prueba práctica de 07-01, ahora vista desde el otro lado.
| No correlacionada | Correlacionada | |
|---|---|---|
| Menciona alias externos | No | Sí |
| Ejecutada sola | Funciona | missing FROM-clause entry |
| Evaluaciones | 1 | 1 por fila candidata |
| Se comporta como | Una constante | Una función de la fila externa |
- El modelo mental: una ejecución por fila
Amplía el diagrama del orden lógico que vienes construyendo desde el módulo 2. La novedad está dentro del paso 2: por cada fila que llega al WHERE, la subconsulta correlacionada se ejecuta entera y devuelve su valor.
flowchart LR
A["1 · FROM / JOIN"] --> B["2 · WHERE<br/>fila a fila"]
B --> S{{"por CADA fila:<br/>ejecutar la subconsulta<br/>con los valores de esa fila"}}
S --> B
B --> C["3 · GROUP BY"] --> D["4 · HAVING"] --> E["5 · SELECT"] --> F["6 · ORDER BY"] --> G["7 · LIMIT"]
Conceptualmente es un bucle anidado: la consulta externa recorre sus filas y, en cada iteración, lanza la consulta interna. Si en el SELECT pones tres subconsultas correlacionadas y la tabla externa tiene 15 filas, son 45 ejecuciones.
Una traza de las primeras filas de productos, con la subconsulta AVG(precio) de su categoría:
| Fila externa | p.categoria_id |
Subconsulta ejecutada | Devuelve | ¿precio > ? |
|---|---|---|---|---|
| 1 · Aceite de oliva, 12,50 € | 1 | AVG(precio) WHERE categoria_id = 1 |
6,18 | sí |
| 2 · Arroz, 3,90 € | 1 | AVG(precio) WHERE categoria_id = 1 |
6,18 | no |
| 3 · Miel, 9,75 € | 1 | AVG(precio) WHERE categoria_id = 1 |
6,18 | sí |
| 4 · Pasta de espelta, 2,80 € | 1 | AVG(precio) WHERE categoria_id = 1 |
6,18 | no |
| 6 · Crema de aloe, 18,90 € | 2 | AVG(precio) WHERE categoria_id = 2 |
11,5375 | sí |
Fíjate en las cuatro primeras filas: el mismo cálculo repetido cuatro veces. Ese desperdicio aparente es el motivo de la sección 7, y también la razón de que los planificadores modernos reescriban muchas correlacionadas.
- El caso canónico: cada producto frente a la media de su categoría
SELECT p.id,
p.nombre AS producto,
cat.nombre AS categoria,
p.precio,
ROUND((SELECT AVG(p2.precio)
FROM productos AS p2
WHERE p2.categoria_id = p.categoria_id), 2) AS media_categoria
FROM productos AS p
JOIN categorias AS cat ON p.categoria_id = cat.id
WHERE p.precio > (SELECT AVG(p2.precio)
FROM productos AS p2
WHERE p2.categoria_id = p.categoria_id)
ORDER BY p.id;| id | producto | categoria | precio | media_categoria |
|---|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | Alimentación | 12.50 | 6.18 |
| 3 | Miel de azahar cruda 500 g | Alimentación | 9.75 | 6.18 |
| 6 | Crema facial de aloe vera 50 ml | Cosmética natural | 18.90 | 11.54 |
| 8 | Aceite corporal de almendras 200 ml | Cosmética natural | 14.25 | 11.54 |
| 10 | Detergente ecológico concentrado 1 L | Hogar sostenible | 11.20 | 10.09 |
| 13 | Velas de cera de soja (pack 2) | Hogar sostenible | 13.75 | 10.09 |
| 15 | Té verde matcha ceremonial 30 g | Bebidas | 22.00 | 8.90 |
| 19 | Desodorante natural en barra 50 g | Higiene personal | 7.80 | 5.65 |
8 productos de 20, y la comparación con 07-01 es muy instructiva: con la media global (9,035 €) salían 9 productos. No son 9 ni un subconjunto de ellos:
| Producto | ¿Supera la media global (9,035)? | ¿Supera la media de su categoría? |
|---|---|---|
| 19 · Desodorante, 7,80 € | No | Sí (5,65 de Higiene personal) |
| 3 · Miel, 9,75 € | Sí | Sí (6,18 de Alimentación) |
| 12 · Bolsas, 9,90 € | Sí | No (10,09 de Hogar sostenible) |
| 20 · Espirulina, 16,40 € | Sí | No (es el único de Complementos: es su propia media) |
El desodorante es barato en términos absolutos pero caro dentro de su categoría; las bolsas son justo lo contrario. Y el caso de la espirulina es el más divertido: es el único producto de Complementos, así que la media de su categoría es su propio precio, y 16.40 > 16.40 es falso. Un producto solo en su categoría nunca puede superar la media de su categoría.
Dos detalles de escritura que conviene fijar:
- La expresión está repetida en el
SELECTy en elWHERE, exactamente como pasaba conHAVINGen 04-06. Es obligatorio (los alias delSELECTno existen en elWHERE) y es feo. La solución limpia son las CTE de 10-02. - Los alias
pyp2son imprescindibles. Dentro y fuera es la misma tablaproductos, y sin alias distintosWHERE categoria_id = categoria_idsería una tautología: la subconsulta ignoraría la categoría, devolvería la media global y la consulta dejaría de estar correlacionada sin dar ningún error. Volveremos a ello en el ejercicio 3.
- La variante: el más caro de su categoría, y la autocorrelación
Cambiando AVG por MAX y > por =, la misma estructura responde a otra pregunta clásica:
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 MAX(p2.precio)
FROM productos AS p2
WHERE p2.categoria_id = p.categoria_id)
ORDER BY p.id;| id | producto | categoria | precio |
|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | Alimentación | 12.50 |
| 6 | Crema facial de aloe vera 50 ml | Cosmética natural | 18.90 |
| 13 | Velas de cera de soja (pack 2) | Hogar sostenible | 13.75 |
| 15 | Té verde matcha ceremonial 30 g | Bebidas | 22.00 |
| 19 | Desodorante natural en barra 50 g | Higiene personal | 7.80 |
| 20 | Cápsulas de espirulina 120 uds | Complementos | 16.40 |
6 filas, una por categoría. Es el patrón greatest-n-per-group, seguramente el más pedido de todo el SQL analítico.
Aquí la tabla externa y la interna son la misma, y eso enlaza directamente con el self join de 03-06: igual que allí unías empleados con empleados para sacar el jefe de cada uno, aquí comparas productos con productos. La diferencia es que el self join produce parejas de filas y la autocorrelación produce un valor calculado para cada fila. Cuando lo que quieres es un agregado por grupo, la correlacionada suele leerse mejor.
Y una advertencia sobre el empate: si dos productos de la misma categoría compartieran el precio máximo, saldrían los dos, porque los dos cumplen la igualdad. Casi siempre es lo que quieres. Si necesitaras exactamente uno por categoría, o el segundo, o un ranking completo, la herramienta correcta ya no es esta: son las funciones de ventana (ROW_NUMBER(), RANK()) de 10-03.
- Cuatro casos más de TiendaVerde
Una subconsulta correlacionada puede ir también en el SELECT, como columna calculada. Esta consulta responde a tres preguntas de golpe:
SELECT c.id,
c.nombre || ' ' || c.apellidos AS cliente,
(SELECT COUNT(*) FROM pedidos AS pe WHERE pe.cliente_id = c.id) AS pedidos,
(SELECT MAX(pe.fecha_pedido) FROM pedidos AS pe WHERE pe.cliente_id = c.id) AS ultimo_pedido,
(SELECT ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2)
FROM pedidos AS pe
JOIN lineas_pedido AS lp ON lp.pedido_id = pe.id
WHERE pe.cliente_id = c.id
GROUP BY pe.id
ORDER BY 1 DESC
LIMIT 1) AS pedido_mas_caro
FROM clientes AS c
ORDER BY c.id;| id | cliente | pedidos | ultimo_pedido | pedido_mas_caro |
|---|---|---|---|---|
| 1 | Lucía Martínez Soler | 3 | 2025-12-02 | 42.10 |
| 2 | Carlos Ferrer Ibáñez | 2 | 2025-09-09 | 32.76 |
| 3 | Marta Sanchis Gil | 1 | 2025-04-02 | 29.53 |
| 4 | Javier Ortega Ruiz | 2 | 2025-12-19 | 31.75 |
| 5 | Ana Belmonte Roca | 2 | 2026-01-27 | 28.10 |
| 6 | Pau Llorens Vidal | 2 | 2026-02-09 | 30.60 |
| 7 | Sofia Moreira Costa | 2 | 2026-01-13 | 64.88 |
| 8 | Tiago Almeida Nunes | 1 | 2025-07-15 | 44.60 |
| 9 | Camille Dubois | 2 | 2026-02-21 | 48.27 |
| 10 | Julien Moreau | 1 | 2025-10-01 | 66.90 |
| 11 | Elena Navarro Puig | 1 | 2025-10-22 | 30.30 |
| 12 | Diego Ramos Herrera | 1 | 2025-11-14 | 31.70 |
| 13 | Núria Bosch Ferrer | 0 | (null) | (null) |
| 14 | Hugo Iglesias Pardo | 0 | (null) | (null) |
| 15 | Inés Carrasco Vega | 0 | (null) | (null) |
Los 15 clientes, incluidos los tres que no han comprado nunca. Eso es una diferencia enorme respecto a un INNER JOIN, que habría devuelto 12 filas: una subconsulta en el SELECT no elimina filas de la consulta externa; devuelve un valor o NULL, pero la fila sigue ahí. Se comporta como un LEFT JOIN sin serlo.
Y observa la asimetría de las tres últimas filas: pedidos vale 0 mientras que las otras dos valen NULL. No es una incoherencia, es 04-04: COUNT sobre un conjunto vacío devuelve 0; MAX y SUM, NULL. Si esa columna va a alimentar un cálculo, envuélvela en COALESCE (06-04).
Cuarto caso: la reseña más reciente de cada producto. Aquí la correlación filtra por producto y LIMIT 1 recorta:
SELECT p.id, p.nombre AS producto,
(SELECT r.fecha FROM resenas AS r WHERE r.producto_id = p.id
ORDER BY r.fecha DESC LIMIT 1) AS ultima_resena,
(SELECT r.puntuacion FROM resenas AS r WHERE r.producto_id = p.id
ORDER BY r.fecha DESC LIMIT 1) AS puntuacion
FROM productos AS p
WHERE EXISTS (SELECT 1 FROM resenas AS r WHERE r.producto_id = p.id)
ORDER BY p.id;| id | producto | ultima_resena | puntuacion |
|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 2025-07-08 | 5 |
| 2 | Arroz integral ecológico 1 kg | 2025-09-19 | 5 |
| 5 | Tomate triturado ecológico 400 g | 2025-04-12 | 3 |
| 6 | Crema facial de aloe vera 50 ml | 2025-08-14 | 4 |
| 10 | Detergente ecológico concentrado 1 L | 2026-01-10 | 4 |
| 12 | Bolsas reutilizables de algodón (pack 5) | 2025-11-03 | 3 |
| 15 | Té verde matcha ceremonial 30 g | 2025-05-02 | 5 |
| 16 | Kombucha de jengibre 750 ml | 2025-06-20 | 2 |
| 18 | Cepillo de dientes de bambú | 2025-07-26 | 4 |
9 productos, los únicos con alguna reseña; los otros 11 los excluye el EXISTS (lección siguiente). Y aquí asoma un defecto real de este patrón: hay dos subconsultas casi idénticas para sacar dos columnas de la misma fila, o sea el doble de trabajo. Eso se resuelve con LATERAL (07-04) o con funciones de ventana (10-03); con una sola columna la escritura de arriba es perfectamente razonable.
- El alcance de los alias: quién ve a quién
La regla es asimétrica y hay que memorizarla:
La subconsulta ve los alias de la consulta externa. La consulta externa NO ve los alias de la subconsulta.
-- ⚠️ INCORRECTA: p2 no existe fuera de la subconsulta
SELECT p.id, p.nombre, p2.precio
FROM productos AS p
WHERE p.precio > (SELECT AVG(p2.precio) FROM productos AS p2 WHERE p2.categoria_id = p.categoria_id);La visibilidad va de dentro hacia fuera, nunca al revés. Piensa en cada subconsulta como una función que recibe la fila externa como parámetro: puede leer lo que le pasan, pero lo que ocurre en su interior no se exporta. Si necesitas una columna de la tabla interna en el resultado, la respuesta no es una subconsulta: es un JOIN (07-05).
Dos consecuencias prácticas:
- Cuando la tabla de dentro y la de fuera son la misma, los alias son obligatorios.
productos AS pfuera,productos AS p2dentro. Sin ellos,WHERE categoria_id = categoria_idse resuelve dentro de la subconsulta y siempre es cierto. - Si un nombre de columna existe en las dos tablas y no lo cualificas, gana la de dentro. Es la regla de resolución de nombres de SQL: primero el ámbito más cercano, luego los exteriores. Es una forma sutilísima de escribir una consulta que "funciona" y responde a otra pregunta. Cualifica siempre todas las columnas dentro de una correlacionada.
- El coste: N ejecuciones y qué hace el planificador
Conceptualmente, una correlacionada es un bucle anidado: N filas externas × 1 ejecución interna. Con productos son 20 ejecuciones; con una tabla de dos millones de filas, dos millones. Si además la subconsulta hace un JOIN, se multiplica el trabajo.
Conceptualmente. Porque PostgreSQL no la ejecuta necesariamente así. El planificador reescribe muchas correlacionadas en formas equivalentes y mucho más baratas:
| Forma escrita | En qué la suele transformar | Efecto |
|---|---|---|
EXISTS (...) correlacionado |
Semi-join (hash o merge) | Una pasada, no N |
NOT EXISTS (...) |
Anti-join | Una pasada |
IN (SELECT ...) |
Semi-join | Una pasada |
Agregado correlacionado en el WHERE |
A veces, agregación + join | Depende |
Agregado correlacionado en el SELECT |
Casi nunca: se ejecuta por fila | N ejecuciones reales |
La última fila es la que importa: las correlacionadas en el SELECT son las que más a menudo se quedan como bucle. Con 15 clientes da igual; con 15 millones, una consulta con tres subconsultas en el SELECT puede tardar minutos donde un LEFT JOIN con GROUP BY tarda segundos.
Comprobarlo de verdad —ver el plan, medir el tiempo, saber si hubo semi-join o bucle— requiere EXPLAIN ANALYZE, y eso es la lección 08-05. Hasta entonces, quédate con la intuición y con esta regla: no reescribas por rendimiento sin medir, pero desconfía de las correlacionadas en el SELECT sobre tablas grandes.
- Cuándo usarla y cuándo delata que falta otra cosa
| Situación | ¿Correlacionada? |
|---|---|
| Comparar cada fila con un agregado de su grupo | Sí, es su caso natural |
Preguntar si existe algo relacionado (EXISTS) |
Sí, y siempre (07-03) |
| Traer "el último", "el primero", "el máximo" de cada fila | Sí, o LATERAL (07-04) |
| Una o dos columnas calculadas sobre una tabla pequeña | Sí, se lee muy bien |
| Cinco columnas calculadas sobre la misma tabla relacionada | No: es un LEFT JOIN + GROUP BY disfrazado |
| Un ranking, un "top 3 por grupo", un acumulado | No: funciones de ventana (10-03) |
| Necesitas columnas de la tabla interna en el resultado | No: es un JOIN (07-05) |
Las dos señales de alarma son claras. Si repites la misma correlación tres o cuatro veces en el SELECT, estás recorriendo la misma tabla tres o cuatro veces para agrupar por la misma clave: eso es un GROUP BY. Y si aparece la palabra "ranking", "posición", "el segundo" o "acumulado", ninguna subconsulta lo hará con elegancia; eso son funciones de ventana.
Reescritura 1: la media por categoría, con tabla derivada
SELECT p.id, p.nombre AS producto, cat.nombre AS categoria,
p.precio, ROUND(m.precio_medio, 2) AS media_categoria
FROM productos AS p
JOIN categorias AS cat ON p.categoria_id = cat.id
JOIN (SELECT categoria_id, AVG(precio) AS precio_medio
FROM productos GROUP BY categoria_id) AS m ON m.categoria_id = p.categoria_id
WHERE p.precio > m.precio_medio
ORDER BY p.id;Devuelve exactamente las mismas 8 filas de la sección 3. La diferencia es que la media de cada categoría se calcula una sola vez —seis medias, en una pasada— en lugar de veinte veces. Además la expresión deja de estar duplicada: se nombra m.precio_medio y se usa dos veces. Esa subconsulta en el FROM se llama tabla derivada y es el contenido de 07-04.
Reescritura 2: el recuento de pedidos, con LEFT JOIN y GROUP BY
SELECT c.id,
c.nombre || ' ' || c.apellidos AS cliente,
COUNT(pe.id) AS pedidos,
MAX(pe.fecha_pedido) AS ultimo_pedido
FROM clientes AS c
LEFT JOIN pedidos AS pe ON pe.cliente_id = c.id
GROUP BY c.id, c.nombre, c.apellidos
ORDER BY c.id;Devuelve las mismas 15 filas que las dos primeras columnas de la sección 5, con los mismos 0 y NULL. Una sola pasada por pedidos en lugar de 30 subconsultas. Cuando las columnas correlacionadas empiezan a acumularse, esta es la reescritura que toca. La comparación honesta de ambas formas —legibilidad, filas, rendimiento— es 07-05.
Errores Comunes y Consejos
- Olvidar los alias cuando la tabla es la misma dentro y fuera.
WHERE categoria_id = categoria_ides una tautología: la correlación desaparece, la subconsulta calcula la media global y no hay ningún error. - No cualificar las columnas dentro de la subconsulta. Si el nombre existe en las dos tablas, gana el ámbito interno y la consulta responde a otra pregunta, en silencio.
- Intentar usar un alias interno en la consulta externa.
missing FROM-clause entry for table "p2". La visibilidad va solo de dentro hacia fuera. - Ejecutar la subconsulta sola para depurarla. No se puede: dará ese mismo error. Para probarla, sustituye a mano el valor externo (
WHERE p2.categoria_id = 1) y comprueba que devuelve lo que esperas. - Esperar que una subconsulta en el
SELECTfiltre filas. No filtra: devuelve un valor oNULL. Los 15 clientes siguen apareciendo. - Confundir el
0deCOUNTcon elNULLdeMAXoSUM. Un cliente sin pedidos tiene 0 pedidos yNULLen cualquier otra columna agregada. - Escribir una escalar correlacionada que devuelve varias filas.
more than one row returned by a subquery used as an expression: la correlación acota, pero no garantiza unicidad. Añade un agregado o unORDER BY ... LIMIT 1. - Consejo: escribe primero la subconsulta con un valor fijo. Comprueba que
AVG(precio) WHERE categoria_id = 1da 6,18 y después sustituye el1porp.categoria_id. Depurar las dos capas a la vez es innecesariamente difícil. - Consejo: si repites la misma correlación en dos columnas, pasa a
LATERALo aGROUP BY. Dos subconsultas idénticas para sacar dos campos de la misma fila son el doble de trabajo por nada. - Consejo: cuenta cuántas veces se ejecutará. Filas de la tabla externa × subconsultas correlacionadas. Si el número te incomoda, plantéate la reescritura de la sección 8 antes de que lo haga producción.
Ejercicios
Ejercicio 1
Calidad quiere saber qué productos tienen una puntuación media superior a la media global de todas las reseñas. Escribe la consulta con una subconsulta no correlacionada para la media global y muestra id, producto, número de reseñas y media (dos decimales), ordenado por media descendente y id.
Después responde: ¿es correlacionada alguna parte de tu consulta? ¿Por qué la respuesta cambiaría si la pregunta fuera "media superior a la media de su categoría"?
Ejercicio 2
Ventas necesita, para cada cliente que haya comprado, el importe de su último pedido (el de fecha más reciente), junto a su nombre y esa fecha. Usa subconsultas correlacionadas. Después indica: ¿qué pasaría si un cliente tuviera dos pedidos el mismo día, y cómo lo arreglarías?
Ejercicio 3
Un compañero ha escrito esto para sacar "los productos por encima de la media de su categoría" y se extraña de que le devuelva las mismas 9 filas que la consulta con la media global de 07-01, en vez de 8:
-- ⚠️ INCORRECTA
SELECT id, nombre, precio
FROM productos
WHERE precio > (SELECT AVG(precio) FROM productos WHERE categoria_id = categoria_id);- ¿Por qué devuelve 9 filas y no 8?
- Corrígela.
- ¿Cuál habría sido el resultado si en lugar de
categoria_id = categoria_idhubiera escritocategoria_id = proveedor_id? Explica el mecanismo, no hace falta el número exacto.
Soluciones
Solución 1
SELECT p.id,
p.nombre AS producto,
COUNT(*) AS resenas,
ROUND(AVG(r.puntuacion), 2) AS media
FROM resenas AS r
JOIN productos AS p ON r.producto_id = p.id
GROUP BY p.id, p.nombre
HAVING AVG(r.puntuacion) > (SELECT AVG(puntuacion) FROM resenas)
ORDER BY media DESC, p.id;| id | producto | resenas | media |
|---|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 2 | 5.00 |
| 15 | Té verde matcha ceremonial 30 g | 1 | 5.00 |
| 2 | Arroz integral ecológico 1 kg | 2 | 4.50 |
| 6 | Crema facial de aloe vera 50 ml | 2 | 4.50 |
4 productos de los 9 reseñados superan la media global, que es 4,0833… (49 puntos entre 12 reseñas). Fuera quedan el detergente y el cepillo (4,00), el tomate y las bolsas (3,00) y la kombucha (2,00).
No hay correlación en ninguna parte: la subconsulta SELECT AVG(puntuacion) FROM resenas no menciona nada de fuera, se ejecuta una vez y da un número. Es el mismo patrón del ticket medio de 07-01. Si la pregunta fuera "superior a la media de su categoría", la subconsulta tendría que filtrar por la categoría del producto del grupo actual —WHERE p2.categoria_id = p.categoria_id— y pasaría a ser correlacionada, evaluándose una vez por grupo.
Solución 2
SELECT c.id,
c.nombre || ' ' || c.apellidos AS cliente,
(SELECT MAX(pe.fecha_pedido) FROM pedidos AS pe WHERE pe.cliente_id = c.id) AS fecha,
(SELECT ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2)
FROM pedidos AS pe
JOIN lineas_pedido AS lp ON lp.pedido_id = pe.id
WHERE pe.cliente_id = c.id
AND pe.fecha_pedido = (SELECT MAX(pe2.fecha_pedido)
FROM pedidos AS pe2 WHERE pe2.cliente_id = c.id)) AS importe
FROM clientes AS c
WHERE EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.cliente_id = c.id)
ORDER BY c.id;| id | cliente | fecha | importe |
|---|---|---|---|
| 1 | Lucía Martínez Soler | 2025-12-02 | 33.40 |
| 2 | Carlos Ferrer Ibáñez | 2025-09-09 | 32.76 |
| 3 | Marta Sanchis Gil | 2025-04-02 | 29.53 |
| 4 | Javier Ortega Ruiz | 2025-12-19 | 31.18 |
| 5 | Ana Belmonte Roca | 2026-01-27 | 28.10 |
| 6 | Pau Llorens Vidal | 2026-02-09 | 26.73 |
| 7 | Sofia Moreira Costa | 2026-01-13 | 47.00 |
| 8 | Tiago Almeida Nunes | 2025-07-15 | 44.60 |
| 9 | Camille Dubois | 2026-02-21 | 22.60 |
| 10 | Julien Moreau | 2025-10-01 | 66.90 |
| 11 | Elena Navarro Puig | 2025-10-22 | 30.30 |
| 12 | Diego Ramos Herrera | 2025-11-14 | 31.70 |
12 filas. Hay tres niveles de anidamiento y dos correlaciones distintas contra c.id. Compara el resultado con el de la sección 5: el último pedido de Lucía vale 33,40 € mientras que su pedido más caro vale 42,10 €; el de Camille son 22,60 € frente a 48,27 €. Son preguntas distintas y conviene no confundirlas en un informe.
Si un cliente tuviera dos pedidos el mismo día, la subconsulta del importe sumaría los dos y devolvería un total inflado (no daría error, porque el GROUP BY no está y SUM agrega todo lo que le llega). El arreglo es desempatar por clave primaria: en lugar de filtrar por fecha_pedido = MAX(...), filtrar por pe.id = (SELECT pe2.id FROM pedidos AS pe2 WHERE pe2.cliente_id = c.id ORDER BY pe2.fecha_pedido DESC, pe2.id DESC LIMIT 1). Cualquier "el último" basado solo en una fecha sin hora es frágil; añade siempre un desempate determinista.
Solución 3
1. Porque categoria_id = categoria_id se resuelve entero dentro de la subconsulta: no hay ningún alias que distinga la tabla de fuera de la de dentro, así que las dos apariciones se refieren a la productos interna. La condición es "una columna igual a sí misma", cierta para las 20 filas, y la subconsulta acaba devolviendo la media global, 9,035 € — de ahí las 9 filas. La consulta no está correlacionada en absoluto, aunque lo parezca, y ese es el fallo: el filtro por categoría no se aplica nunca. Es un error silencioso de manual.
2. La corrección es la de la sección 3: alias distintos dentro y fuera.
-- ✅ CORRECTA
SELECT p.id, p.nombre, p.precio
FROM productos AS p
WHERE p.precio > (SELECT AVG(p2.precio) FROM productos AS p2
WHERE p2.categoria_id = p.categoria_id);8 filas, las de la sección 3.
3. categoria_id = proveedor_id tampoco correlaciona: compara dos columnas de la misma tabla interna. La subconsulta devolvería la media de los productos cuya categoría coincide numéricamente con su proveedor —una condición sin ningún sentido de negocio pero perfectamente válida—, y ese único número se aplicaría a las 20 filas. Es el peor de los casos: una consulta que no da error, parece correlacionada, y responde a una pregunta que nadie ha hecho. De ahí la regla de cualificar siempre las columnas.
Conclusión
La correlación cambia la naturaleza de una subconsulta:
- Una subconsulta correlacionada referencia un alias de la consulta externa; no se puede ejecutar sola (
missing FROM-clause entry) y se evalúa una vez por cada fila candidata, como un bucle anidado. - El caso canónico es comparar cada fila con un agregado de su propio grupo: 8 productos superan la media de su categoría, frente a los 9 que superaban la media global — y no son los mismos, porque el desodorante es caro para Higiene personal y las bolsas son baratas para Hogar sostenible.
- La variante con
= MAX(...)da el más caro de cada categoría (6 filas, una por categoría), el patrón greatest-n-per-group, hermano de la autocorrelación y del self join de 03-06. - En el
SELECTfunciona como columna calculada y no elimina filas: los 15 clientes siguen ahí, con0enCOUNTyNULLenMAXySUM. - La visibilidad es asimétrica: la subconsulta ve los alias de fuera, la externa no ve los de dentro. Cuando la tabla es la misma, los alias distintos son obligatorios: sin ellos la correlación se evapora en silencio.
- Conceptualmente cuesta N ejecuciones. PostgreSQL reescribe muchas como semi-joins o anti-joins, pero las correlacionadas en el
SELECTsuelen quedarse como bucle; medirlo de verdad es 08-05. - Y sabes reconocer cuándo no es la herramienta: varias correlaciones repetidas piden un
LEFT JOINconGROUP BY, y cualquier ranking pide funciones de ventana (10-03).
En la lección siguiente, EXISTS y NOT EXISTS, verás la forma más pura de subconsulta correlacionada: una que no devuelve ningún valor, solo responde sí o no. Con ella escribirás por fin "clientes que no han comprado nunca" sin LEFT JOIN, descubrirás por qué NOT EXISTS es seguro con nulos y NOT IN no, y resolverás el problema clásico de la división relacional: qué clientes han comprado de todas las categorías.
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
