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
- La regla base
- Equivalencia 1:
INfrente aINNER JOIN(y la diferencia real) - Equivalencia 2: las cuatro formas de responder "qué no casa"
- Equivalencia 3: escalar en el
SELECTfrente aLEFT JOIN+GROUP BY - Equivalencia 4: correlacionada frente a tabla derivada
- Qué hace el optimizador por debajo
- Legibilidad y mantenimiento, criterio de primera clase
- Tabla-guía: quiero X → escribe Y
- Errores Comunes y Consejos
- Ejercicios
- Conclusión del módulo
- 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.
- Equivalencia 1:
IN frente a INNER JOIN (y la diferencia real)
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:
INes una prueba de pertenencia: pregunta "¿este valor está en la lista?" y responde una vez por fila externa. No puede multiplicar.JOINes 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
JOINsí es la respuesta correcta: si quisieras "clientes con la fecha de cada pedido", las 20 filas son el resultado que buscas, porquefecha_pedidoes una columna de la otra tabla. La regla base en acción.
- 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 -- 4NOT 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.
- Equivalencia 3: escalar en el
SELECT frente a LEFT JOIN + GROUP BY
SELECT frente a LEFT JOIN + GROUP BYYa 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 JOINconlineas_pedidoobliga aCOUNT(DISTINCT pe.id)para no contar 9 pedidos donde hay 3. - El
GROUP BYes 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).
- 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 | Sí, SELECT y WHERE |
No: se nombra una vez |
| Se lee como la pregunta | Sí, 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.
- 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:
INyEXISTSfrente a unJOINson, en rendimiento, prácticamente lo mismo en PostgreSQL moderno. Elige por legibilidad, no por velocidad.NOT INsí es más lento, además de peligroso, porque el optimizador tiene las manos atadas por la semántica de los nulos.- Las correlacionadas en el
SELECTson 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 ANALYZEte enseña el plan real y los tiempos, y es la lección 08-05. Hasta entonces, escribe la versión más legible.
- 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 elSELECTy en elHAVINGes deuda técnica. - Tres o más subconsultas casi idénticas en el
SELECT. Es unGROUP BYesperando a nacer.
- 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
JOINpara una pregunta de existencia. Multiplica filas y te obliga a unDISTINCTque oculta el problema: 20 filas donde querías 12. - Creer que "las subconsultas son lentas". En PostgreSQL 16,
INyEXISTSse 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 INporque "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,WHEREyHAVING. 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.
- Productos con su categoría y su proveedor.
- Productos que han recibido alguna reseña.
- Pedidos cuyo importe supera el ticket medio global.
- Clientes con el número de pedidos que han hecho.
- 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;- ¿El resultado es correcto? ¿Cuántas filas devuelve antes y después del
DISTINCT? - Reescríbela con
EXISTSy explica qué se gana. - ¿En qué caso el
JOINserí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;- ¿Cuántas ejecuciones de subconsulta implica?
- Reescríbela con
JOINyGROUP BY, cuidando el recuento. - 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 escribesJOIN ... DISTINCT, queríasEXISTS. INfrente aINNER JOINno es una cuestión de estilo: son 12 filas frente a 20.INprueba pertenencia y responde una vez por fila; elJOINproduce una fila por pareja.- Las cuatro formas de responder "qué no casa" quedan zanjadas:
NOT EXISTSpor defecto, anti-join si ya unías esas tablas,EXCEPTpara comparar conjuntos, yNOT INnunca 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 pidenLEFT JOIN+GROUP BY, cuidando elCOUNT(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,EXISTSyNOT EXISTScomo semi-joins y anti-joins, así que su rendimiento suele ser equivalente al delJOIN;NOT INy las correlacionadas en elSELECTson las que de verdad se pagan. Y nada de esto se supone: se mide conEXPLAIN 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
- ¿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
