Esta lección no enseña sintaxis nueva. Enseña a decidir. Después de cuatro lecciones tienes dos cajas de herramientas que se solapan: casi todas las preguntas que sabes responder con un JOIN admiten también una subconsulta, y al revés. Y el problema de tener dos formas de escribir lo mismo es que nadie te dice cuál usar.

Hay una regla base que resuelve el 80 % de los casos en cinco segundos, cuatro grandes equivalencias que conviene conocer con sus dos escrituras, una diferencia real —no de estilo— entre IN y un INNER JOIN, y un criterio de rendimiento que hay que manejar con humildad: el optimizador reescribe muchas de estas consultas, y suponer cuál es más rápida sin medir es la forma más común de perder el tiempo.

Contenido

  1. La regla base
  2. Equivalencia 1: IN frente a INNER JOIN (y la diferencia real)
  3. Equivalencia 2: las cuatro formas de responder "qué no casa"
  4. Equivalencia 3: escalar en el SELECT frente a LEFT JOIN + GROUP BY
  5. Equivalencia 4: correlacionada frente a tabla derivada
  6. Qué hace el optimizador por debajo
  7. Legibilidad y mantenimiento, criterio de primera clase
  8. Tabla-guía: quiero X → escribe Y
  9. Errores Comunes y Consejos
  10. Ejercicios
  11. Conclusión del módulo

  1. La regla base

Si necesitas columnas de la otra tabla en el resultado, es un JOIN. Si solo necesitas filtrar o calcular un valor, suele ser una subconsulta.

Es sorprendentemente fiable. Léela como una pregunta sobre el SELECT, no sobre el WHERE: mira qué columnas quieres mostrar y de dónde salen.

La pregunta Qué quieres mostrar Herramienta
"Pedidos con el nombre de su cliente" Columnas de pedidos y de clientes JOIN
"Clientes que han comprado alguna vez" Solo columnas de clientes Subconsulta (EXISTS)
"Productos por encima del precio medio" Solo columnas de productos Subconsulta escalar
"Líneas de pedido con producto y categoría" Tres tablas en el resultado JOIN
"Categorías que facturan más que la media" categorias + un agregado propio GROUP BY + subconsulta en HAVING

Y su corolario, que es igual de útil: si te descubres escribiendo un JOIN seguido de un DISTINCT, casi siempre querías un EXISTS. El DISTINCT es el síntoma de que has traído filas que no necesitabas para responder a una pregunta de sí o no.

  1. Equivalencia 1: IN frente a INNER JOIN (y la diferencia real)

La pregunta: clientes que han hecho algún pedido.

-- Versión A: subconsulta
SELECT c.id, c.nombre || ' ' || c.apellidos AS cliente, c.pais
FROM clientes AS c
WHERE c.id IN (SELECT cliente_id FROM pedidos)
ORDER BY c.id;

-- Versión B: INNER JOIN
SELECT c.id, c.nombre || ' ' || c.apellidos AS cliente, c.pais
FROM clientes AS c
JOIN pedidos AS pe ON pe.cliente_id = c.id
ORDER BY c.id;

Parecen la misma consulta. No lo son. La versión A devuelve una fila por cliente:

id cliente pais
1 Lucía Martínez Soler España
2 Carlos Ferrer Ibáñez España
3 Marta Sanchis Gil España
4 Javier Ortega Ruiz España

(4 primeras de 12 filas.)

Versión A (IN) Versión B (JOIN)
Filas devueltas 12 20
Clientes distintos 12 12
Lucía Martínez Soler aparece… 1 vez 3 veces

La versión B devuelve las 20 filas de pedidos con los datos del cliente repetidos:

id cliente pais
1 Lucía Martínez Soler España
1 Lucía Martínez Soler España
1 Lucía Martínez Soler España
2 Carlos Ferrer Ibáñez España
2 Carlos Ferrer Ibáñez España
3 Marta Sanchis Gil España

(6 primeras de 20 filas.) Esta es la diferencia de fondo entre las dos herramientas, y no es cuestión de gusto:

  • IN es una prueba de pertenencia: pregunta "¿este valor está en la lista?" y responde una vez por fila externa. No puede multiplicar.
  • JOIN es un producto filtrado: genera una fila por cada pareja que casa. Si la tabla derecha tiene tres coincidencias, salen tres filas.

Para igualarlas hay que añadir SELECT DISTINCT a la versión B — y ahí es donde el DISTINCT delata el problema. Además, DISTINCT obliga a ordenar o a construir una tabla hash sobre las 20 filas ya generadas: has hecho trabajo de más para luego deshacerlo.

El caso en que el JOIN sí es la respuesta correcta: si quisieras "clientes con la fecha de cada pedido", las 20 filas son el resultado que buscas, porque fecha_pedido es una columna de la otra tabla. La regla base en acción.

  1. Equivalencia 2: las cuatro formas de responder "qué no casa"

Aquí se cierra definitivamente el hilo abierto en 03-07 y desarrollado en 07-03. La pregunta —clientes que no han comprado nunca— tiene cuatro escrituras, todas correctas, todas identificando a los mismos tres clientes (Núria, Hugo e Inés):

WHERE NOT EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.cliente_id = c.id)   -- 1
WHERE c.id NOT IN (SELECT cliente_id FROM pedidos)                          -- 2
LEFT JOIN pedidos AS pe ON pe.cliente_id = c.id WHERE pe.id IS NULL         -- 3
SELECT id FROM clientes EXCEPT SELECT cliente_id FROM pedidos               -- 4
NOT EXISTS NOT IN Anti-join EXCEPT
Seguridad con NULL ✅ Total 0 filas si hay nulos ✅ Total ✅ Total
Columnas en el resultado Todas las externas Todas las externas Todas las externas ❌ Solo las comparadas
Duplicados No los crea No los crea No los crea Los elimina siempre
Legibilidad ✅ Se lee como la pregunta ✅ Muy alta… hasta que falla ⚠️ Hay que conocer el idiom ✅ Alta
Plan típico en PostgreSQL Anti-join Peor: filtro no anti-joinable Anti-join Sort + dedup
Portabilidad ✅ Universal ✅ Universal ✅ Universal ⚠️ MINUS en Oracle
Veredicto Por defecto Solo con NOT NULL garantizado Buena si ya unías Comparar conjuntos

La recomendación, en una línea: NOT EXISTS por defecto; anti-join si ya estabas uniendo esas tablas por otro motivo; EXCEPT para comparar conjuntos; NOT IN, nunca por costumbre.

La razón de descartar NOT IN como opción por defecto no es que sea peor hoy: es que su corrección depende de una propiedad del esquema —que la columna sea NOT NULL— que puede cambiar sin que nadie revise tus consultas. Una migración que permita nulos en empleado_id no dará ningún error y tus informes empezarán a devolver cero filas.

  1. Equivalencia 3: escalar en el SELECT frente a LEFT JOIN + GROUP BY

Ya viste las dos escrituras en 07-04. Aquí interesa el criterio:

Número de métricas Escritura preferible Por qué
1 Subconsulta en el SELECT Se lee de un vistazo; no toca el FROM
2 Cualquiera Empate técnico
3 o más LEFT JOIN + GROUP BY Una pasada en vez de N; una sola definición del FROM

El caso de una sola métrica, enfrentado:

-- Versión A: escalar en el SELECT
SELECT c.id, c.nombre,
       (SELECT COUNT(*) FROM pedidos AS pe WHERE pe.cliente_id = c.id) AS pedidos
FROM clientes AS c
ORDER BY c.id;

-- Versión B: LEFT JOIN + GROUP BY
SELECT c.id, c.nombre, COUNT(pe.id) AS pedidos
FROM clientes AS c
LEFT JOIN pedidos AS pe ON pe.cliente_id = c.id
GROUP BY c.id, c.nombre
ORDER BY c.id;
id nombre pedidos
1 Lucía 3
2 Carlos 2
3 Marta 1
13 Núria 0

(4 filas de las 15; idénticas en las dos versiones, incluidos los ceros de los clientes 13, 14 y 15.) Con una métrica la versión A gana en claridad: no toca el FROM, no necesita GROUP BY y no puede inflar nada. Y dos matices que suelen decidir la elección cuando hay más:

  • La subconsulta es inmune a la multiplicación de filas. El LEFT JOIN con lineas_pedido obliga a COUNT(DISTINCT pe.id) para no contar 9 pedidos donde hay 3.
  • El GROUP BY es inmune a la repetición. Añadir una métrica nueva es una línea; con subconsultas es copiar un bloque de cuatro líneas y cambiar el agregado.

Hay un tercer caso donde la subconsulta gana claramente: cuando la métrica no se puede expresar con un agregado sobre el JOIN. "El importe del último pedido de cada cliente" no es ningún SUM ni MAX de las columnas unidas: exige ordenar y cortar. Ahí toca subconsulta correlacionada, LATERAL (07-04) o función de ventana (10-03).

  1. Equivalencia 4: correlacionada frente a tabla derivada

La pregunta: productos por encima de la media de su categoría (las 8 filas de 07-02).

-- Versión A: correlacionada
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);

-- Versión B: tabla derivada agregada
SELECT p.id, p.nombre, p.precio, ROUND(m.precio_medio, 2) AS media_categoria
FROM productos AS p
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;

Las mismas 8 filas, y la versión B además muestra el umbral:

id nombre precio media_categoria
1 Aceite de oliva virgen extra 500 ml 12.50 6.18
3 Miel de azahar cruda 500 g 9.75 6.18
6 Crema facial de aloe vera 50 ml 18.90 11.54
8 Aceite corporal de almendras 200 ml 14.25 11.54

(4 primeras de 8 filas; cierran los productos 10, 13, 15 y 19.) Las diferencias:

Correlacionada Tabla derivada
Veces que se calcula la media 20 (conceptualmente) 6, una por categoría
¿Puedes mostrar la media? Sí, repitiendo la subconsulta Sí, gratis: es una columna
Expresión duplicada , SELECT y WHERE No: se nombra una vez
Se lee como la pregunta , casi literalmente Menos directo
Escala a tablas grandes Peor Mejor

Con pocas filas y una sola comparación, la correlacionada es más legible. En cuanto quieras mostrar el valor de referencia, reutilizarlo o aplicarlo sobre millones de filas, la tabla derivada es mejor — y con tres pasos, la CTE de 10-02 es mejor que las dos.

  1. Qué hace el optimizador por debajo

Antes de decidir por rendimiento, conviene saber que el motor no ejecuta lo que escribes, sino un plan equivalente que él elige. PostgreSQL aplica varias transformaciones automáticas:

Lo que escribes En qué lo suele convertir ¿Cambia el rendimiento?
IN (SELECT ...) Semi-join (hash o merge) No: acaba siendo como un JOIN deduplicado
EXISTS (...) Semi-join No
NOT EXISTS (...) Anti-join No
NOT IN (SELECT ...) No puede: debe conservar la semántica de los nulos Sí, a peor
Tabla derivada simple La aplana (subquery pull-up) dentro de la consulta externa No
Escalar correlacionada en el SELECT Casi nunca la transforma Sí, a peor con muchas filas
Escalar correlacionada en el WHERE A veces sí, a veces no Depende

De aquí salen las tres conclusiones prácticas del módulo:

  1. IN y EXISTS frente a un JOIN son, en rendimiento, prácticamente lo mismo en PostgreSQL moderno. Elige por legibilidad, no por velocidad.
  2. NOT IN sí es más lento, además de peligroso, porque el optimizador tiene las manos atadas por la semántica de los nulos.
  3. Las correlacionadas en el SELECT son la construcción con más riesgo real: son las que más a menudo se ejecutan literalmente, una vez por fila.

No optimices a ciegas. "Las subconsultas son lentas" es un mito que circula desde MySQL 5.5, donde efectivamente lo eran. Con PostgreSQL 16 casi nunca es cierto, y con otros motores depende de la versión. La única forma honesta de saberlo es medir: EXPLAIN ANALYZE te enseña el plan real y los tiempos, y es la lección 08-05. Hasta entonces, escribe la versión más legible.

  1. Legibilidad y mantenimiento, criterio de primera clase

Cuando dos escrituras tienen el mismo rendimiento —que es lo habitual—, la legibilidad no es un criterio secundario: es el criterio. Una consulta se escribe una vez y se lee, se depura y se modifica decenas de veces.

Compara. La versión anidada de "clientes cuyo ticket medio supera la media global", con tres niveles:

SELECT c.nombre, t.ticket_medio
FROM clientes AS c
JOIN (SELECT pe.cliente_id, AVG(p.total) AS ticket_medio
      FROM (SELECT pe2.id, pe2.cliente_id,
                   SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)) AS total
            FROM pedidos AS pe2 JOIN lineas_pedido AS lp ON lp.pedido_id = pe2.id
            GROUP BY pe2.id, pe2.cliente_id) AS p
      JOIN pedidos AS pe ON pe.id = p.id
      GROUP BY pe.cliente_id) AS t ON t.cliente_id = c.id
WHERE t.ticket_medio > (SELECT AVG(x.total) FROM (SELECT SUM(...) AS total ...) AS x);

Se lee de dentro hacia fuera, la sangría consume media pantalla y el cálculo del total de pedido está escrito dos veces. La misma lógica con CTE:

WITH totales_pedido AS (...),          -- total de cada pedido, una sola vez
     media_global    AS (...),          -- 36,40 €
     ticket_cliente  AS (...)           -- media por cliente
SELECT ... FROM ticket_cliente WHERE ticket_medio > (SELECT * FROM media_global);

Cada paso tiene nombre, se lee de arriba abajo como un procedimiento, y totales_pedido se define una vez y se usa dos. Eso es 10-02, y es la razón por la que este módulo insiste en remitir allí: las subconsultas resuelven el problema, las CTE lo dejan legible.

Tres señales de que una consulta pide reescritura:

  • Más de dos niveles de anidamiento. Cuenta paréntesis de apertura seguidos.
  • La misma expresión escrita dos o más veces. Un SUM(...) repetido en el SELECT y en el HAVING es deuda técnica.
  • Tres o más subconsultas casi idénticas en el SELECT. Es un GROUP BY esperando a nacer.

  1. Tabla-guía: quiero X → escribe Y

Quiero… Escribe
Columnas de dos tablas relacionadas INNER JOIN
Columnas de A, y las de B si existen LEFT JOIN
Filas de A que tienen relación en B, sin duplicar EXISTS (o IN)
Filas de A que no tienen relación en B NOT EXISTS
Comparar cada fila con un valor global Subconsulta escalar en el WHERE
Comparar cada fila con un valor de su grupo Correlacionada, o derivada + JOIN
Filtrar grupos por un valor global Subconsulta escalar en el HAVING
Una métrica calculada por fila Escalar en el SELECT
Tres o más métricas por fila LEFT JOIN + GROUP BY
Agregar sobre un agregado Tabla derivada en el FROM
Métricas de granularidad distinta Dos tablas derivadas unidas
El "top N" de cada grupo LATERAL, o función de ventana (10-03)
Reutilizar un cálculo o encadenar 3+ pasos CTE con WITH (10-02)
Un ranking, una posición, un acumulado Función de ventana (10-03)

Errores Comunes y Consejos

  • Usar JOIN para una pregunta de existencia. Multiplica filas y te obliga a un DISTINCT que oculta el problema: 20 filas donde querías 12.
  • Creer que "las subconsultas son lentas". En PostgreSQL 16, IN y EXISTS se convierten en semi-joins. El mito viene de motores y versiones antiguos.
  • Reescribir por rendimiento sin medir. Cambiar una consulta legible por una críptica basándote en una intuición es la peor operación posible: pierdes legibilidad y puede que no ganes nada (08-05).
  • Mantener NOT IN porque "hoy funciona". Su corrección depende de una propiedad del esquema que puede cambiar sin avisar.
  • Anidar tres niveles cuando existe una CTE. Funciona, pero nadie —tú incluido— podrá modificarla dentro de seis meses.
  • Repetir la misma expresión en SELECT, WHERE y HAVING. Cada copia es una oportunidad de que una se actualice y las otras no.
  • Consejo: escribe primero la versión que se parezca a la pregunta. Si la pregunta dice "clientes que no han comprado", escribe NOT EXISTS. La consulta que se lee como el enunciado es la que menos errores esconde.
  • Consejo: cuenta las filas de cada versión antes de dar una por buena. Si dos escrituras "equivalentes" devuelven 12 y 20 filas, no eran equivalentes.
  • Consejo: guarda las dos versiones cuando dudes. Deja la alternativa comentada con una nota de por qué elegiste la otra. Es la documentación más barata que existe.

Ejercicios

Ejercicio 1

Para cada una de estas cinco preguntas, decide JOIN o subconsulta aplicando la regla base, y justifica en una frase. No hace falta escribir el SQL completo.

  1. Productos con su categoría y su proveedor.
  2. Productos que han recibido alguna reseña.
  3. Pedidos cuyo importe supera el ticket medio global.
  4. Clientes con el número de pedidos que han hecho.
  5. Empleados que nunca han gestionado un pedido.

Ejercicio 2

Un compañero ha escrito esto para "los productos que se han vendido alguna vez":

-- ⚠️ Sospechosa
SELECT DISTINCT p.id, p.nombre, p.precio
FROM productos AS p
JOIN lineas_pedido AS lp ON lp.producto_id = p.id
ORDER BY p.id;
  1. ¿El resultado es correcto? ¿Cuántas filas devuelve antes y después del DISTINCT?
  2. Reescríbela con EXISTS y explica qué se gana.
  3. ¿En qué caso el JOIN sería la escritura correcta para una pregunta parecida?

Ejercicio 3

Toma esta consulta, que responde a "clientes con más de un pedido y su facturación total":

SELECT c.id, c.nombre,
       (SELECT COUNT(*) FROM pedidos pe WHERE pe.cliente_id = c.id) AS pedidos,
       (SELECT ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2)
        FROM pedidos pe JOIN lineas_pedido lp ON lp.pedido_id = pe.id
        WHERE pe.cliente_id = c.id) AS facturacion
FROM clientes c
WHERE (SELECT COUNT(*) FROM pedidos pe WHERE pe.cliente_id = c.id) > 1
ORDER BY facturacion DESC;
  1. ¿Cuántas ejecuciones de subconsulta implica?
  2. Reescríbela con JOIN y GROUP BY, cuidando el recuento.
  3. Da el resultado y di qué versión defenderías en una revisión de código.

Soluciones

Solución 1

# Pregunta Elección Por qué
1 Productos con categoría y proveedor JOIN (dos) El resultado muestra columnas de las tres tablas
2 Productos con alguna reseña Subconsulta (EXISTS) Solo se muestran columnas de productos; el JOIN duplicaría el aceite, el arroz y la crema
3 Pedidos por encima del ticket medio Subconsulta escalar + tabla derivada El umbral es un valor calculado, no una tabla que unir
4 Clientes con su número de pedidos Las dos Una métrica: escalar en el SELECT o LEFT JOIN + GROUP BY. Si hicieran falta tres métricas, GROUP BY sin dudarlo
5 Empleados sin ningún pedido Subconsulta (NOT EXISTS) Pregunta de ausencia, y empleado_id admite nulos: NOT IN daría 0 filas

Solución 2

1. El resultado es correcto, pero por accidente del DISTINCT. Sin él, el JOIN devuelve 47 filas —una por línea de pedido—, con el aceite de oliva repetido 5 veces y el arroz 4. Con DISTINCT quedan 17 filas, los 17 productos vendidos. Es decir: el motor genera 47 filas, las ordena o las mete en una tabla hash, y descarta 30. Trabajo hecho para deshacerlo.

2. Con EXISTS:

-- ✅ CORRECTA
SELECT p.id, p.nombre, p.precio
FROM productos AS p
WHERE EXISTS (SELECT 1 FROM lineas_pedido AS lp WHERE lp.producto_id = p.id)
ORDER BY p.id;

17 filas directamente, sin generar las 47 ni deduplicar. Se gana: el resultado no puede duplicarse por construcción, la intención queda explícita ("productos que tienen alguna venta"), el EXISTS cortocircuita en la primera línea encontrada, y el DISTINCT desaparece —con él, el riesgo de que alguien añada mañana una columna de lineas_pedido al SELECT y el DISTINCT deje de deduplicar sin que nadie lo note.

3. El JOIN sería correcto en cuanto la pregunta pidiera algo de lineas_pedido: "productos vendidos con las unidades de cada venta" (47 filas, y son el resultado), o "productos vendidos con el total de unidades" (17 filas, con GROUP BY). En cuanto necesites datos de la otra tabla, la regla base manda.

Solución 3

1. Las ejecuciones. Tres subconsultas por cliente —dos en el SELECT y una repetida en el WHERE— × 15 clientes = 45, de las cuales 15 son un COUNT calculado dos veces por cliente. Ese COUNT duplicado es el síntoma clásico: el WHERE no ve los alias del SELECT (orden lógico del módulo 2), así que hay que repetir la expresión entera.

2. La reescritura:

-- ✅ Una sola pasada
SELECT c.id,
       c.nombre,
       COUNT(DISTINCT pe.id) AS pedidos,
       ROUND(SUM(lp.cantidad * lp.precio_unitario * (1 - lp.descuento)), 2) AS facturacion
FROM clientes AS c
JOIN pedidos       AS pe ON pe.cliente_id = c.id
JOIN lineas_pedido AS lp ON lp.pedido_id  = pe.id
GROUP BY c.id, c.nombre
HAVING COUNT(DISTINCT pe.id) > 1
ORDER BY facturacion DESC;
id nombre pedidos facturacion
7 Sofia 2 111.88
1 Lucía 3 107.60
9 Camille 2 70.87
4 Javier 2 62.93
2 Carlos 2 59.46
6 Pau 2 57.33
5 Ana 2 54.85

7 clientes, los mismos que salían en 04-06 con HAVING COUNT(*) > 1. Dos detalles obligatorios: COUNT(DISTINCT pe.id), porque el segundo JOIN multiplica cada pedido por sus líneas (Lucía tendría 9 pedidos en vez de 3); y INNER JOIN en lugar de LEFT, que aquí es correcto porque la condición > 1 ya excluye a quien tiene 0.

3. Cuál defender. La versión con GROUP BY, sin dudarlo: una pasada en lugar de 45, la expresión del importe escrita una sola vez y el criterio de corte visible en el HAVING junto a la columna que muestra. La versión con subconsultas tiene una única ventaja real —no necesita COUNT(DISTINCT), porque no multiplica filas— y esa ventaja no compensa repetir el COUNT en dos sitios. Si la consulta creciera a cinco métricas, la diferencia dejaría de ser discutible.

(Y la versión ideal, con el total de pedido calculado una sola vez y nombrado, llega en 10-02.)

Conclusión del módulo

Cierras el módulo 7 con un criterio, no solo con sintaxis:

  • La regla base: si necesitas columnas de la otra tabla, JOIN; si solo filtras o calculas un valor, subconsulta. Y si escribes JOIN ... DISTINCT, querías EXISTS.
  • IN frente a INNER JOIN no es una cuestión de estilo: son 12 filas frente a 20. IN prueba pertenencia y responde una vez por fila; el JOIN produce una fila por pareja.
  • Las cuatro formas de responder "qué no casa" quedan zanjadas: NOT EXISTS por defecto, anti-join si ya unías esas tablas, EXCEPT para comparar conjuntos, y NOT IN nunca por costumbre — porque su corrección depende de una propiedad del esquema que puede cambiar sin avisar.
  • Una métrica por fila cabe bien en una escalar del SELECT; tres o más piden LEFT JOIN + GROUP BY, cuidando el COUNT(DISTINCT).
  • Correlacionada o tabla derivada: la primera se lee como la pregunta, la segunda calcula seis medias en vez de veinte y te regala la columna de referencia.
  • El optimizador reescribe IN, EXISTS y NOT EXISTS como semi-joins y anti-joins, así que su rendimiento suele ser equivalente al del JOIN; NOT IN y las correlacionadas en el SELECT son las que de verdad se pagan. Y nada de esto se supone: se mide con EXPLAIN ANALYZE (08-05).
  • La legibilidad es un criterio de primera clase: tres niveles de anidamiento o una expresión repetida son señales de que toca una CTE (10-02).

Y con esto se cierra el módulo 7. Repasa lo que has ganado en cinco lecciones: distingues una subconsulta no correlacionada de una correlacionada y sabes que la segunda se evalúa una vez por fila; reconoces las escalares, las de fila y las de tabla, y los operadores que admite cada una; has resuelto por fin la pregunta que 04-06 dejó pendiente —Julien, Sofia y Tiago superan el ticket medio de 36,40 €—; dominas EXISTS y NOT EXISTS, incluida la doble negación de la división relacional; colocas subconsultas en el SELECT, en el FROM, en el WHERE, en el HAVING y en las instrucciones del módulo 5; y con las tablas derivadas has liquidado por fin el problema de los portes que arrastrabas desde el módulo 3: 727,95 € + 118,25 € = 846,20 €, cuadrado al céntimo.

Con los siete módulos que llevas, puedes escribir casi cualquier consulta que TiendaVerde necesite. Y justo por eso, la pregunta importante cambia. Hasta ahora ha sido siempre la misma: ¿esto devuelve lo que quiero?. A partir de aquí es otra: ¿cuánto tarda?. Con 20 pedidos, 47 líneas y 15 clientes, todo lo que has escrito responde en milisegundos, y da exactamente igual que una subconsulta se ejecute veinte veces o una sola. Con 20 millones de pedidos, esa misma consulta puede tardar minutos, bloquear una pantalla de administración o tumbar un informe nocturno. En el módulo 8, Índices y rendimiento, aprenderás qué es un índice y cómo convierte un recorrido completo de la tabla en una búsqueda dirigida, cómo crearlos y gestionarlos, qué tipos existen y —tan importante como lo anterior— cuándo no indexar, las técnicas de optimización de consultas, y EXPLAIN, la herramienta que te dirá por fin, con datos y no con intuiciones, qué está haciendo realmente el motor con el SQL que escribes.

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