Hay una familia de preguntas que no necesita ningún valor: solo un sí o un no. ¿Ha comprado este cliente alguna vez? ¿Tiene este producto alguna reseña? ¿Contiene este pedido algún artículo de cosmética? No importa cuántos, ni cuáles, ni cuánto suman: importa si existe al menos uno.
EXISTS es el operador que responde exactamente a eso, y es probablemente la subconsulta que más vas a escribir en tu vida profesional. Su negación, NOT EXISTS, resuelve la otra mitad —clientes que no han comprado, productos que nadie ha vendido— y lo hace con una propiedad que ninguna de las alternativas tiene: es inmune a los valores nulos. En esta lección verás las dos, las compararás con NOT IN, con el anti-join de 03-03 y con EXCEPT de 03-07, y cerrarás con el problema clásico de la división relacional.
Contenido
EXISTS: un predicado, no un valor- Por qué da igual lo que ponga en el
SELECTinterno - Cuatro casos de TiendaVerde con
EXISTS NOT EXISTS: los cuatro huecos del conjunto de datosNOT EXISTSfrente aNOT IN: la diferencia que importa- Las cuatro formas de responder "qué no casa"
- Cortocircuito:
EXISTSfrente aCOUNT(*) > 0 - División relacional: la doble negación
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
EXISTS: un predicado, no un valor
EXISTS: un predicado, no un valorEXISTS (subconsulta) es un predicado: devuelve TRUE o FALSE, nunca NULL. Su regla es de una simplicidad radical:
EXISTSesTRUEen cuanto la subconsulta produce su primera fila. Si no produce ninguna, esFALSE.
SELECT c.id, c.nombre, c.apellidos
FROM clientes AS c
WHERE EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.cliente_id = c.id)
ORDER BY c.id
LIMIT 4;| id | nombre | apellidos |
|---|---|---|
| 1 | Lucía | Martínez Soler |
| 2 | Carlos | Ferrer Ibáñez |
| 3 | Marta | Sanchis Gil |
| 4 | Javier | Ortega Ruiz |
(4 primeras de 12 filas: los 12 clientes que han comprado alguna vez.)
Tres propiedades lo distinguen de todo lo visto: devuelve TRUE/FALSE y nunca NULL, así que es inmune a la lógica de tres valores de 04-03; no mira el contenido de la subconsulta, da igual qué columnas devuelva; y se detiene en la primera fila, no cuenta ni suma ni ordena.
Y una cuarta, decisiva: EXISTS es correlacionado en la práctica siempre. Un EXISTS sin correlación —WHERE EXISTS (SELECT 1 FROM pedidos)— es TRUE o FALSE para todas las filas por igual, así que devuelve toda la tabla o ninguna fila. No es un error, pero tampoco sirve de nada. La condición pe.cliente_id = c.id es la que hace el trabajo: convierte "¿hay pedidos?" en "¿hay pedidos de este cliente?".
- Por qué da igual lo que ponga en el
SELECT interno
SELECT internoEstas cuatro escrituras son exactamente equivalentes y producen el mismo plan de ejecución:
WHERE EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.cliente_id = c.id)
WHERE EXISTS (SELECT * FROM pedidos AS pe WHERE pe.cliente_id = c.id)
WHERE EXISTS (SELECT pe.id FROM pedidos AS pe WHERE pe.cliente_id = c.id)
WHERE EXISTS (SELECT 1/0 FROM pedidos AS pe WHERE pe.cliente_id = c.id)La última es la demostración: 1/0 es una división por cero y no da error, porque PostgreSQL nunca evalúa la lista de columnas de un EXISTS. Solo comprueba si la consulta produce filas. La proyección se descarta antes de calcularse.
De ahí el idiom SELECT 1, el que verás en el 90 % del código profesional: es la forma más corta de decir "no me interesa el contenido". SELECT * es igual de correcto y algunas guías lo prefieren porque subraya lo mismo. Elige una y sé coherente; en este curso usamos SELECT 1.
Lo que sí importa dentro del
EXISTSes elWHERE: la correlación. UnEXISTScuyoWHEREno menciona la fila externa está mal planteado casi seguro.
- Cuatro casos de TiendaVerde con
EXISTS
EXISTSProductos con al menos una reseña. El EXISTS no dice cuántas ni de qué puntuación: solo que hay alguna.
SELECT p.id, p.nombre AS producto, p.precio
FROM productos AS p
WHERE EXISTS (SELECT 1 FROM resenas AS r WHERE r.producto_id = p.id)
ORDER BY p.id;| id | producto | precio |
|---|---|---|
| 1 | Aceite de oliva virgen extra 500 ml | 12.50 |
| 2 | Arroz integral ecológico 1 kg | 3.90 |
| 5 | Tomate triturado ecológico 400 g | 1.95 |
| 6 | Crema facial de aloe vera 50 ml | 18.90 |
| 10 | Detergente ecológico concentrado 1 L | 11.20 |
(5 primeras de 9 filas; las otras cuatro son los productos 12, 15, 16 y 18.) 9 de los 20 productos. Compáralo con un INNER JOIN sobre resenas: ese habría devuelto 12 filas, una por reseña, con el aceite, el arroz y la crema repetidos. EXISTS nunca multiplica filas, y esa es su ventaja más práctica frente al JOIN.
Pedidos que contienen algún producto de cosmética natural. Aquí la subconsulta lleva su propio JOIN:
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 EXISTS (SELECT 1
FROM lineas_pedido AS lp
JOIN productos AS p ON p.id = lp.producto_id
WHERE lp.pedido_id = pe.id
AND p.categoria_id = 2)
ORDER BY pe.id;| pedido_id | fecha_pedido | cliente | estado |
|---|---|---|---|
| 2 | 2025-03-12 | Carlos Ferrer Ibáñez | entregado |
| 6 | 2025-05-23 | Ana Belmonte Roca | cancelado |
| 9 | 2025-07-15 | Tiago Almeida Nunes | entregado |
| 10 | 2025-08-03 | Camille Dubois | entregado |
| 15 | 2025-12-02 | Lucía Martínez Soler | entregado |
| 18 | 2026-01-27 | Ana Belmonte Roca | pagado |
6 pedidos de 20. El pedido 9 contiene dos artículos de cosmética (champú y bálsamo) y aparece una sola vez: con JOIN habrían salido 7 filas y habría hecho falta un DISTINCT. Empleados que han gestionado algún pedido, mismo patrón:
SELECT e.id, e.nombre || ' ' || e.apellidos AS empleado, e.puesto
FROM empleados AS e
WHERE EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.empleado_id = e.id)
ORDER BY e.id;| id | empleado | puesto |
|---|---|---|
| 4 | Óscar Peris Blasco | Comercial |
| 5 | Laia Puig Sanchis | Comercial |
| 6 | Marc Estévez Roig | Atención al cliente |
3 de los 8 empleados, exactamente los que anunciaba 01-06.
Nota de dialecto: en PostgreSQL
EXISTSdevuelve unbooleande verdad, así que puedes usarlo como columna:SELECT c.id, EXISTS (SELECT 1 FROM pedidos pe WHERE pe.cliente_id = c.id) AS ha_comprado FROM clientes cdevuelvetrue/falsepara los 15 clientes. SQL Server y Oracle no lo permiten y obligan a envolverlo en unCASE WHEN EXISTS (...) THEN 1 ELSE 0 END. MySQL 8 y SQLite sí lo aceptan, devolviendo 1 o 0.
NOT EXISTS: los cuatro huecos del conjunto de datos
NOT EXISTS: los cuatro huecos del conjunto de datosNOT EXISTS es la negación literal: TRUE cuando la subconsulta no produce ninguna fila. Es la herramienta natural para todas las preguntas con "sin", "nunca" o "ninguno", y responde a los cuatro huecos deliberados de TiendaVerde con la misma plantilla.
-- Clientes que no han comprado nunca
SELECT c.id, c.nombre, c.apellidos, c.ciudad, c.fecha_registro
FROM clientes AS c
WHERE NOT EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.cliente_id = c.id)
ORDER BY c.id;| id | nombre | apellidos | ciudad | fecha_registro |
|---|---|---|---|---|
| 13 | Núria | Bosch Ferrer | Barcelona | 2025-06-20 |
| 14 | Hugo | Iglesias Pardo | Zaragoza | 2025-09-12 |
| 15 | Inés | Carrasco Vega | Valencia | 2026-01-08 |
Y la misma plantilla, cambiando solo la tabla de dentro, da los productos nunca vendidos:
SELECT p.id, p.nombre, p.precio, p.stock, p.activo
FROM productos AS p
WHERE NOT EXISTS (SELECT 1 FROM lineas_pedido AS lp WHERE lp.producto_id = p.id)
ORDER BY p.id;| id | nombre | precio | stock | activo |
|---|---|---|---|---|
| 13 | Velas de cera de soja (pack 2) | 13.75 | 0 | true |
| 19 | Desodorante natural en barra 50 g | 7.80 | 75 | true |
| 20 | Cápsulas de espirulina 120 uds | 16.40 | 55 | false |
Con NOT EXISTS (SELECT 1 FROM resenas AS r WHERE r.producto_id = p.id) sobre productos obtienes los 11 productos sin reseña, y con NOT EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.empleado_id = e.id) sobre empleados, los 5 empleados sin pedidos (Rosa, Andrés, Beatriz, Irene y Daniel).
Cuatro preguntas de negocio, una sola plantilla. Esa uniformidad es la razón principal para preferir NOT EXISTS: no hay que decidir qué columna comprobar con IS NULL, ni preocuparse por los nulos, ni recordar qué tabla va a la izquierda.
NOT EXISTS frente a NOT IN: la diferencia que importa
NOT EXISTS frente a NOT IN: la diferencia que importaAhora la demostración central del módulo. La misma pregunta, dos escrituras, dos resultados distintos.
-- ⚠️ INCORRECTA: 0 filas
SELECT e.id, e.nombre, e.apellidos
FROM empleados AS e
WHERE e.id NOT IN (SELECT empleado_id FROM pedidos);
-- ✅ CORRECTA: 5 filas
SELECT e.id, e.nombre, e.apellidos
FROM empleados AS e
WHERE NOT EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.empleado_id = e.id);| id | nombre | apellidos |
|---|---|---|
| 1 | Rosa | Alcázar Vives |
| 2 | Andrés | Company Talens |
| 3 | Beatriz | Nadal Ripoll |
| 7 | Irene | Salvador Mira |
| 8 | Daniel | Vercher Lluch |
La primera devuelve cero filas y la segunda cinco. La causa, que ya diagnosticaste en 07-01, son los 10 pedidos del canal web con empleado_id a NULL: la lista es {4, 5, 6, NULL} y 1 <> NULL es UNKNOWN, así que NOT IN no puede ser TRUE para ninguna fila. NOT EXISTS no sufre lo mismo porque no compara el valor externo con una lista: pregunta si la subconsulta produce filas. Para el empleado 1, SELECT 1 FROM pedidos WHERE empleado_id = 1 no produce ninguna —las filas con empleado_id nulo tampoco satisfacen esa igualdad y se descartan como cualquier otra que no case— y NOT EXISTS es TRUE. La lógica de tres valores actúa dentro de la subconsulta, donde solo decide qué filas se devuelven, y nunca escapa al predicado.
flowchart LR
A["e.id = 1"] --> B{"NOT IN (4,5,6,NULL)"}
B --> C["UNKNOWN → ❌ descartada"]
A --> D{"NOT EXISTS<br/>(pedidos con empleado_id = 1)"}
D --> E["0 filas → TRUE → ✅ conservada"]
La regla, en una frase:
NOT EXISTSes seguro con nulos yNOT INno. Si eligesNOT EXISTSpor costumbre, nunca tendrás que preguntarte si la columna de la subconsulta admite nulos.
- Las cuatro formas de responder "qué no casa"
En 03-07 se anunció que las cuatro escrituras se compararían en el módulo 7. Aquí están, resolviendo la misma pregunta —clientes que no han comprado nunca— y devolviendo todas las mismas 3 filas (13, 14 y 15):
-- 1. NOT EXISTS
SELECT c.id, c.nombre FROM clientes AS c
WHERE NOT EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.cliente_id = c.id);
-- 2. NOT IN
SELECT c.id, c.nombre FROM clientes AS c
WHERE c.id NOT IN (SELECT cliente_id FROM pedidos);
-- 3. Anti-join (03-03)
SELECT c.id, c.nombre FROM clientes AS c
LEFT JOIN pedidos AS pe ON pe.cliente_id = c.id WHERE pe.id IS NULL;
-- 4. EXCEPT (03-07)
SELECT id FROM clientes EXCEPT SELECT cliente_id FROM pedidos;NOT EXISTS |
NOT IN |
Anti-join | EXCEPT |
|
|---|---|---|---|---|
Comportamiento con NULL |
Seguro | Peligroso: 0 filas | Seguro | Seguro (trata NULL como un valor más) |
| Columnas que puede devolver | Todas las de fuera | Todas las de fuera | Todas las de fuera | Solo las comparadas |
| ¿Elimina duplicados? | No los crea | No los crea | No los crea | Sí, siempre |
| Legibilidad | Alta: se lee como la pregunta | Muy alta… hasta que falla | Media: hay que saber el idiom | Alta, pero limitada |
| Rendimiento típico | Anti-join en el plan | Peor si hay nulos posibles | Anti-join en el plan | Requiere ordenar/dedup |
| Portabilidad | Universal | Universal | Universal | EXCEPT no existe en MySQL 5.7 ni en Oracle (allí es MINUS) |
La recomendación del curso: usa NOT EXISTS. Es seguro, universal, deja disponibles todas las columnas de la tabla externa y se lee igual que la pregunta de negocio. El anti-join es igual de idiomático y a veces más natural si ya estabas uniendo esas tablas. EXCEPT, solo cuando compares conjuntos de la misma forma y no necesites más columnas. Y NOT IN, únicamente si garantizas que la columna de la subconsulta es NOT NULL — y aun entonces, no ganas nada. Esta tabla se cierra y se amplía con el criterio de rendimiento en 07-05.
- Cortocircuito:
EXISTS frente a COUNT(*) > 0
EXISTS frente a COUNT(*) > 0Es tentador escribir "¿hay alguno?" como "¿el recuento es mayor que cero?". Funciona y da el mismo resultado —12 filas las dos—, pero no el mismo trabajo:
-- ⚠️ Correcta pero peor
SELECT c.id, c.nombre FROM clientes AS c
WHERE (SELECT COUNT(*) FROM pedidos AS pe WHERE pe.cliente_id = c.id) > 0;
-- ✅ Preferible
SELECT c.id, c.nombre FROM clientes AS c
WHERE EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.cliente_id = c.id);COUNT(*) > 0 |
EXISTS |
|
|---|---|---|
| Filas que lee la subconsulta | Todas las del cliente | Una: se detiene en la primera |
| Con un cliente de 50.000 pedidos | Cuenta 50.000 | Lee 1 |
| Qué comunica al lector | "cuántos hay, y compáralo" | "¿hay alguno?" |
EXISTS cortocircuita: en cuanto encuentra una fila, deja de buscar; COUNT no puede, porque para saber cuántas hay tiene que verlas todas. Con 20 pedidos la diferencia es inmedible; con tablas reales separa una consulta instantánea de un recorrido completo. Y hay un tercer argumento, el más importante en el día a día: EXISTS dice lo que quieres decir. Lo mismo con la negación: COUNT(*) = 0 es NOT EXISTS escrito de forma más cara.
- División relacional: la doble negación
Llegamos al problema clásico. La pregunta parece inocente:
¿Qué clientes han comprado productos de todas las categorías?
Y no se puede responder con EXISTS a secas, porque EXISTS habla de "alguno", no de "todos". El truco es reformular la frase hasta convertir el "todos" en dos "ningunos" encadenados:
Esa reformulación —que en lógica es ∀x P(x) ≡ ¬∃x ¬P(x)— se traduce literalmente a SQL:
SELECT c.id, c.nombre || ' ' || c.apellidos AS cliente
FROM clientes AS c
WHERE NOT EXISTS ( -- no hay ninguna categoría…
SELECT 1
FROM categorias AS cat
WHERE NOT EXISTS ( -- …de la que este cliente no haya comprado
SELECT 1
FROM pedidos AS pe
JOIN lineas_pedido AS lp ON lp.pedido_id = pe.id
JOIN productos AS p ON p.id = lp.producto_id
WHERE pe.cliente_id = c.id
AND p.categoria_id = cat.id))
ORDER BY c.id;Ningún cliente, y el resultado no es decepcionante sino diagnóstico: Complementos no ha vendido nunca (0,00 € en las cifras del curso), así que nadie puede haber comprado de las seis categorías. La consulta es correcta; lo que falla es la pregunta. Reformulada sobre las cuatro categorías principales —Alimentación, Cosmética natural, Hogar sostenible y Bebidas— basta con acotar la subconsulta intermedia:
| id | cliente |
|---|---|
| 1 | Lucía Martínez Soler |
Una sola clienta. Lucía es la única que ha comprado de las cuatro categorías principales, y lo ha conseguido con tres pedidos muy distintos entre sí: el 1 (aceite, arroz e infusión), el 5 (detergente, estropajo y bolsas) y el 15 (miel, champú y estropajo). Es exactamente el perfil que un equipo de marketing querría identificar: el cliente que ha explorado el catálogo entero.
Cómo se lee esta consulta sin marearse
Léela de dentro hacia fuera, y en tres frases:
| Nivel | Qué pregunta | Para quién |
|---|---|---|
Interno (NOT EXISTS 2) |
"¿este cliente no ha comprado nada de esta categoría?" | Cada pareja cliente-categoría |
Intermedio (FROM categorias) |
"¿hay alguna categoría que cumpla lo anterior?" | Cada cliente |
Externo (NOT EXISTS 1) |
"¿ninguna la cumple?" → entonces las compró todas | Cada cliente |
Y una advertencia: hay dos correlaciones en juego. La interna usa c.id (dos niveles hacia fuera) y cat.id (uno). Si olvidas una, la consulta sigue siendo válida y devuelve una barbaridad. Prueba la subconsulta interna con valores fijos (c.id = 1, cat.id = 3) antes de ensamblar las tres capas.
Nota: hay una alternativa más legible con
GROUP BYyHAVING COUNT(DISTINCT p.categoria_id) = 4, más corta y a menudo más rápida. La doble negación merece entenderse porque es la única que funciona cuando el conjunto de referencia no es un simple recuento —"todas las categorías activas", "todos los productos de un catálogo que cambia"— y porque es el idiom que reconocerás al leer código ajeno.
Errores Comunes y Consejos
- Escribir un
EXISTSsin correlación.WHERE EXISTS (SELECT 1 FROM pedidos)esTRUEpara todas las filas y devuelve la tabla entera: la condición que enlaza con la fila externa es lo esencial. - Creer que
SELECT *dentro de unEXISTSes más lento. No lo es: la lista de columnas nunca se evalúa, ySELECT 1ySELECT *producen el mismo plan. - Usar
NOT INsobre una columna que admite nulos. Cero filas, sin ningún aviso;NOT EXISTSno tiene ese problema nunca. Y escribirCOUNT(*) > 0cuenta todas las filas para responder a algo que se contesta con la primera. - Poner un
ORDER BYo unLIMITdentro de unEXISTS. No cambian el resultado y añaden trabajo. Y confundirNOT EXISTSconEXISTS (... WHERE NOT ...). "No tiene ningún pedido entregado" esNOT EXISTS (... estado = 'entregado'); "tiene algún pedido no entregado" esEXISTS (... estado <> 'entregado'). Son preguntas distintas. - Consejo: traduce la pregunta palabra por palabra. "Clientes que han comprado" →
EXISTS. "Clientes que no han comprado nunca" →NOT EXISTS. "Clientes que han comprado de todas" →NOT EXISTS ( ... NOT EXISTS ( ... )). Y ahí, cuidado con olvidar una de las dos correlaciones: no da error y el resultado es basura. - Consejo: usa
EXISTScuando elJOINte obligaría a unDISTINCT. Si solo quieres saber si hay relación y no necesitas datos de la otra tabla, evita la multiplicación de filas de raíz. Y prueba siempre las subconsultas con valores fijos —sustituyec.idpor1— antes de montar la correlación.
Ejercicios
Ejercicio 1
Atención al cliente quiere una campaña de opiniones dirigida a clientes que han comprado alguna vez pero nunca han escrito una reseña. Escribe la consulta con EXISTS y NOT EXISTS en la misma cláusula WHERE, mostrando id, nombre completo, país y número de pedidos.
Después responde: ¿cuántos clientes quedan fuera por cada una de las dos condiciones?
Ejercicio 2
Un compañero necesita los pedidos que no contienen ningún producto de alimentación y ha escrito esto:
-- ⚠️ Sospechosa
SELECT DISTINCT pe.id
FROM pedidos AS pe
JOIN lineas_pedido AS lp ON lp.pedido_id = pe.id
JOIN productos AS p ON p.id = lp.producto_id
WHERE p.categoria_id <> 1;- Explica por qué está mal y qué pregunta responde realmente.
- Escríbela correctamente con
NOT EXISTSy da el número de filas. - ¿Podrías resolverla con un anti-join? ¿Y con
NOT IN? Justifica si serían seguras.
Ejercicio 3
Compras quiere saber qué proveedores no tienen ningún producto sin vender: aquellos de los que todas sus referencias se han vendido al menos una vez. Escríbelo con doble NOT EXISTS. (Pista: misma estructura que la división relacional, pero el conjunto de referencia son los productos del propio proveedor.)
Soluciones
Solución 1
SELECT c.id,
c.nombre || ' ' || c.apellidos AS cliente,
c.pais,
(SELECT COUNT(*) FROM pedidos AS pe WHERE pe.cliente_id = c.id) AS pedidos
FROM clientes AS c
WHERE EXISTS (SELECT 1 FROM pedidos AS pe WHERE pe.cliente_id = c.id)
AND NOT EXISTS (SELECT 1 FROM resenas AS r WHERE r.cliente_id = c.id)
ORDER BY c.id;| id | cliente | pais | pedidos |
|---|---|---|---|
| 5 | Ana Belmonte Roca | España | 2 |
| 10 | Julien Moreau | Francia | 1 |
| 12 | Diego Ramos Herrera | España | 1 |
3 clientes. El reparto de los 15:
| Condición | Descarta | Quiénes |
|---|---|---|
EXISTS (pedidos) |
3 | Núria, Hugo e Inés: no han comprado nunca, no tienen nada que reseñar |
NOT EXISTS (resenas) |
9 | Los 9 clientes que ya han escrito alguna reseña |
| Sobreviven | 3 | Ana, Julien y Diego |
Los tres son objetivos perfectos: han comprado, están satisfechos o no lo sabemos, y nunca les hemos pedido su opinión. Fíjate en que las dos condiciones son inseparables: solo con NOT EXISTS (resenas) saldrían 6 clientes, incluyendo a tres que no han comprado nada.
Solución 2
1. Qué está mal. La consulta responde a "pedidos que contienen algún producto que no es de alimentación", que es casi lo contrario. Un pedido con aceite (categoría 1) y kombucha (categoría 4) tiene una línea que cumple categoria_id <> 1, así que aparece — aunque sí lleva alimentación. El error es de cuantificador: el WHERE de un JOIN filtra líneas, y la pregunta habla de pedidos. El DISTINCT disimula el síntoma (las filas repetidas) sin tocar la causa.
2. La versión correcta:
-- ✅ CORRECTA
SELECT pe.id AS pedido_id, pe.fecha_pedido, pe.estado
FROM pedidos AS pe
WHERE NOT EXISTS (SELECT 1
FROM lineas_pedido AS lp
JOIN productos AS p ON p.id = lp.producto_id
WHERE lp.pedido_id = pe.id
AND p.categoria_id = 1)
ORDER BY pe.id;| pedido_id | fecha_pedido | estado |
|---|---|---|
| 2 | 2025-03-12 | entregado |
| 5 | 2025-05-07 | entregado |
| 7 | 2025-06-11 | entregado |
| 9 | 2025-07-15 | entregado |
| 10 | 2025-08-03 | entregado |
(5 primeras de 10 filas; las otras son los pedidos 12, 13, 16, 18 y 19.) 10 pedidos de 20 no llevan ni un solo producto de alimentación. La consulta original devolvía 18 —todos los que tienen alguna línea de otra categoría—, y ocho de ellos sí compran alimentación. Observa el cambio de lógica: la condición p.categoria_id = 1 se escribe en positivo dentro del EXISTS, y la negación la aporta el NOT. Ese es el patrón de toda pregunta con "ninguno".
3. Con anti-join, sí: FROM pedidos pe LEFT JOIN (lineas_pedido lp JOIN productos p ON p.id = lp.producto_id AND p.categoria_id = 1) ON lp.pedido_id = pe.id WHERE lp.id IS NULL funciona, pero hay que llevar la condición de categoría al ON (si va al WHERE degrada el LEFT a INNER, 03-03), lo que la hace bastante más frágil de escribir. Con NOT IN también funcionaría —pe.id NOT IN (SELECT lp.pedido_id FROM lineas_pedido lp JOIN productos p ON ... WHERE p.categoria_id = 1)— y sería segura porque lineas_pedido.pedido_id es NOT NULL. Pero seguirías dependiendo de una garantía del esquema que mañana puede cambiar: NOT EXISTS no depende de nada.
Solución 3
SELECT pr.id, pr.nombre AS proveedor, pr.pais
FROM proveedores AS pr
WHERE NOT EXISTS (
SELECT 1
FROM productos AS p
WHERE p.proveedor_id = pr.id
AND NOT EXISTS (SELECT 1 FROM lineas_pedido AS lp
WHERE lp.producto_id = p.id))
ORDER BY pr.id;| id | proveedor | pais |
|---|---|---|
| 1 | Huerta del Turia | España |
| 2 | BioSierra Ibérica | España |
| 3 | Verde Atlántico | Portugal |
3 proveedores de 5. Cuadra con los tres productos nunca vendidos: el 19 (Desodorante) es de Maison Nature, y el 13 (Velas) y el 20 (Espirulina) son de EcoNordic Supplies. Esos dos quedan fuera; los otros tres han colocado su catálogo entero.
La estructura es idéntica a la de la sección 8 con una única diferencia: el conjunto de referencia está correlacionado (p.proveedor_id = pr.id) en lugar de ser la tabla entera de categorías. Es la variante más útil del patrón en la práctica, y su lectura literal es "no hay ningún producto suyo del que no exista ninguna venta".
Conclusión
EXISTS es la subconsulta que más vas a escribir:
EXISTSes un predicado: devuelveTRUEen cuanto la subconsulta produce una fila, y nuncaNULL. No mira las columnas delSELECTinterno —de ahí el idiomSELECT 1, e inclusoSELECT 1/0funciona— y cortocircuita.- En la práctica siempre es correlacionado: la condición que enlaza con la fila externa es lo que le da sentido.
- No multiplica filas. Los 9 productos con reseña salen 9 veces, no 12; los 6 pedidos con cosmética salen 6, no 7. Donde un
JOINpediríaDISTINCT,EXISTSno lo necesita. NOT EXISTSresuelve los cuatro huecos de TiendaVerde con una sola plantilla: 3 clientes sin comprar, 3 productos sin vender, 11 productos sin reseña y 5 empleados sin pedidos. Y sobre todo: es seguro con nulos yNOT INno — la misma pregunta da 0 filas conNOT INy 5 conNOT EXISTS, por los 10 pedidos web conempleado_idnulo.- Conoces las cuatro formas de responder "qué no casa" —
NOT EXISTS,NOT IN, anti-join yEXCEPT— con sus diferencias en nulos, columnas disponibles, duplicados y portabilidad; la recomendación esNOT EXISTS. Y sabes traducir un "todos" con la doble negación: ningún cliente ha comprado de las seis categorías (Complementos no vendió nunca) y solo Lucía lo ha hecho de las cuatro principales.
En la lección siguiente, subconsultas en SELECT, FROM y WHERE, dejarás de mirar el qué para mirar el dónde: qué cambia según la cláusula en la que colocas la subconsulta, por qué una tabla derivada en el FROM necesita alias obligatoriamente, cómo se agrega dos veces seguidas para llegar por fin al ticket medio de 36,40 € desde su origen, y cómo se cruzan dos agregados de granularidad distinta para que los 727,95 € de producto y los 118,25 € de portes cuadren sin inflarse.
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
