EXISTS

El operador EXISTS comprueba si una subconsulta devuelve al menos una fila. No le importa qué columnas o valores contiene el resultado, solo si hay filas o no. Devuelve TRUE si la subconsulta tiene resultados y FALSE si está vacía.

EXISTS se usa casi siempre con subconsultas correlacionadas y es especialmente eficiente porque MySQL puede dejar de buscar en cuanto encuentra la primera fila que cumple la condición.

Sintaxis

SELECT columnas
FROM tabla
WHERE EXISTS (subconsulta);

La subconsulta dentro de EXISTS es normalmente correlacionada: hace referencia a la consulta exterior. Por convención, se escribe SELECT 1 dentro del EXISTS porque el valor concreto no importa (solo importa si hay filas o no).

Clientes con pedidos

Encontrar clientes que han realizado al menos un pedido:

SELECT cl.nombre, cl.apellidos
FROM clientes cl
WHERE EXISTS (
    SELECT 1
    FROM pedidos p
    WHERE p.cliente_id = cl.id
)
ORDER BY cl.nombre;
nombreapellidos
AlejandroSerrano Blanco
AnaMartínez Ruiz
AndrésVega Romero
CarlosRodríguez Martín
CarmenRuiz Jiménez
DavidSánchez Moreno
ElenaRomero Navarro
FernandoMolina Ortega
IsabelGil Molina
JavierMoreno Díaz
LauraLópez Sánchez
LucíaDíaz Hernández
MaríaGarcía López
MartaBlanco Castro
MiguelHernández Torres
PaulaOrtega Serrano
PedroFernández Castro
RobertoNavarro Gil
SaraTorres Vega

Para cada cliente, MySQL ejecuta la subconsulta buscando si existe al menos un pedido con ese cliente_id. En cuanto encuentra uno, devuelve TRUE y pasa al siguiente cliente. No necesita contar cuántos pedidos hay ni leer todos. Esto hace que EXISTS sea muy eficiente.

19 de los 20 clientes han hecho al menos un pedido. Diego es el único que falta.

NOT EXISTS

NOT EXISTS es la negación: devuelve TRUE cuando la subconsulta no encuentra ninguna fila. Es el patrón ideal para encontrar registros sin relación:

SELECT cl.nombre, cl.apellidos, cl.email
FROM clientes cl
WHERE NOT EXISTS (
    SELECT 1
    FROM pedidos p
    WHERE p.cliente_id = cl.id
);
nombreapellidosemail
DiegoCastro Iglesiasdiego.castro@email.com

Diego es el único cliente sin pedidos. Este patrón es equivalente al LEFT JOIN ... WHERE IS NULL que vimos en la sección de JOINs, pero muchos desarrolladores consideran que NOT EXISTS expresa la intención de forma más clara.

Productos sin reseñas

SELECT p.nombre, p.precio
FROM productos p
WHERE NOT EXISTS (
    SELECT 1
    FROM resenas r
    WHERE r.producto_id = p.id
)
ORDER BY p.precio DESC
LIMIT 5;
nombreprecio
Lenovo ThinkPad X11549.00
Sofá 3 plazas599.00
Estantería modular149.99
Zapatillas trail149.99
Vaqueros slim fit59.99

Los 5 productos más caros que nadie ha reseñado. El Lenovo ThinkPad lidera la lista con 1549 euros y ninguna opinión.

EXISTS vs IN

EXISTS y IN pueden resolver problemas similares. Clientes con pedidos entregados:

-- Con EXISTS
SELECT cl.nombre
FROM clientes cl
WHERE EXISTS (
    SELECT 1
    FROM pedidos p
    WHERE p.cliente_id = cl.id
      AND p.estado = 'entregado'
)
ORDER BY cl.nombre;
 
-- Con IN (resultado equivalente)
SELECT cl.nombre
FROM clientes cl
WHERE cl.id IN (
    SELECT cliente_id
    FROM pedidos
    WHERE estado = 'entregado'
)
ORDER BY cl.nombre;
nombre
Ana
Carlos
Isabel
Laura
María
Pedro

Ambas devuelven los mismos 6 clientes. La diferencia está en cómo MySQL las ejecuta internamente:

EXISTS es generalmente más rápido cuando la tabla exterior es pequeña y la interior es grande, porque MySQL puede dejar de buscar al encontrar la primera coincidencia. IN puede ser más rápido cuando la subconsulta devuelve pocos valores, porque MySQL puede optimizar la lista de valores en memoria.

En la práctica, las versiones modernas de MySQL optimizan ambas formas de manera similar en la mayoría de casos.

NOT EXISTS vs NOT IN

Aquí sí hay una diferencia importante. NOT IN tiene un problema con NULLs:

-- NOT IN: si la subconsulta devuelve algún NULL, el resultado es vacío
SELECT nombre FROM clientes
WHERE id NOT IN (SELECT cliente_id FROM pedidos);
-- Funciona porque cliente_id no tiene NULLs
 
-- NOT EXISTS: funciona siempre, sin importar los NULLs
SELECT nombre FROM clientes cl
WHERE NOT EXISTS (
    SELECT 1 FROM pedidos p WHERE p.cliente_id = cl.id
);

Si la columna cliente_id pudiera contener NULLs, NOT IN devolvería un resultado vacío (como explicamos en el artículo de NOT IN). NOT EXISTS no tiene este problema porque no compara valores, solo comprueba si hay filas.

Por esta razón, NOT EXISTS es la opción más segura y recomendada para buscar "los que no tienen".

EXISTS con múltiples condiciones

Puedes añadir condiciones complejas dentro del EXISTS. Clientes que han hecho al menos un pedido de más de 500 euros que fue entregado:

SELECT cl.nombre, cl.apellidos
FROM clientes cl
WHERE EXISTS (
    SELECT 1
    FROM pedidos p
    WHERE p.cliente_id = cl.id
      AND p.total > 500
      AND p.estado = 'entregado'
)
ORDER BY cl.nombre;
nombreapellidos
CarlosRodríguez Martín
MaríaGarcía López
PedroFernández Castro

Solo 3 clientes cumplen todas las condiciones: haber hecho un pedido de más de 500 euros que además fue entregado.

EXISTS doble

Puedes combinar varios EXISTS para condiciones más elaboradas. Clientes que han comprado y han dejado reseñas:

SELECT cl.nombre, cl.apellidos
FROM clientes cl
WHERE EXISTS (
    SELECT 1 FROM pedidos p WHERE p.cliente_id = cl.id
)
AND EXISTS (
    SELECT 1 FROM resenas r WHERE r.cliente_id = cl.id
)
ORDER BY cl.nombre;

Esto es equivalente al INTERSECT que vimos en la sección anterior, pero usando EXISTS. La ventaja de EXISTS es que funciona en todas las versiones de MySQL, mientras que INTERSECT requiere 8.0.31+.

Practica con EXISTS

Usa el editor para verificar la existencia de filas relacionadas:

Simulador SQL
Ctrl+Enter para ejecutar

En el siguiente artículo exploraremos las tablas derivadas en detalle, una técnica que usa subconsultas en el FROM para crear resultados intermedios.

Escrito por Eduardo Lázaro