sqlpostgresqlintersectset-operators

INTERSECT en SQL: filas presentes en ambas consultas

Como el operador INTERSECT encuentra filas comunes a dos consultas, en que se diferencia de INTERSECT ALL y cuando conviene un JOIN o EXISTS.

3 min de lecturaReferencesql · postgresql · intersect · set-operators · mysql

El operador INTERSECT devuelve solo las filas que aparecen en el resultado de ambas consultas a la vez. Es la forma mas directa de responder "quien esta en los dos conjuntos" sin montar un join a mano, y es uno de los tres operadores de conjuntos de SQL junto con UNION y EXCEPT.

Sintaxis basica

INTERSECT se coloca entre dos sentencias SELECT y compara filas completas, columna a columna. Ambas consultas deben devolver el mismo numero de columnas con tipos compatibles.

-- Users who both placed an order and requested a refund
SELECT user_id FROM purchases
INTERSECT
SELECT user_id FROM refunds;

El resultado es un conjunto ordenado de valores user_id unicos presentes en cada entrada. Un ejemplo concreto sobre nuestro esquema: paises que tienen a la vez usuarios registrados y pedidos pagados.

SELECT country FROM users
INTERSECT
SELECT u.country
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid';

Propiedades clave del operador:

  • Se comparan todas las columnas y NULL se considera igual a NULL (a diferencia de un = normal).
  • Los duplicados se eliminan por defecto, igual que con DISTINCT.
  • El orden de las columnas importa: la primera se empareja con la primera, la segunda con la segunda.

INTERSECT frente a INTERSECT ALL

Por defecto INTERSECT elimina duplicados. Cuando necesitas una fila repetida tantas veces como aparece en ambas entradas, usa INTERSECT ALL: conserva el numero minimo de ocurrencias de cada lado.

-- DISTINCT semantics: each matching id appears once
SELECT user_id FROM orders WHERE amount > 100
INTERSECT
SELECT user_id FROM orders WHERE status = 'paid';

-- Multiplicity preserved: min(count_left, count_right) copies
SELECT user_id FROM orders WHERE amount > 100
INTERSECT ALL
SELECT user_id FROM orders WHERE status = 'paid';

Si user_id = 7 aparece 3 veces a la izquierda y 2 a la derecha, INTERSECT ALL lo devuelve dos veces, mientras que el INTERSECT simple lo devuelve una sola vez. En la practica la forma sin ALL es la que mas se usa: normalmente te interesa si una fila pertenece a ambos conjuntos, no cuantas copias hay. Y no consideres INTERSECT ALL "gratis": igualmente tiene que emparejar filas y contar ocurrencias en cada lado para conservar min(count_left, count_right) copias. El unico ahorro frente al INTERSECT simple es que se omite la deduplicacion final; el emparejamiento y el conteo siguen ahi.

Trampa: en PostgreSQL INTERSECT tiene mayor prioridad que UNION y EXCEPT. En una cadena como A UNION B INTERSECT C, el motor evalua primero B INTERSECT C. Usa parentesis explicitos para no tener que adivinar.

INTERSECT frente a JOIN y EXISTS

La misma "pertenencia comun" se puede expresar con un INNER JOIN o con EXISTS, pero la semantica difiere en los detalles.

-- INNER JOIN can multiply rows if either side has duplicates
SELECT DISTINCT a.user_id
FROM orders a
JOIN refunds b ON a.user_id = b.user_id;

-- EXISTS: clean, no row multiplication, handles NULL via predicate
SELECT DISTINCT o.user_id
FROM orders o
WHERE EXISTS (
  SELECT 1 FROM refunds r WHERE r.user_id = o.user_id
);

Como elegir:

  • INTERSECT cuando comparas conjuntos de filas completos y quieres deduplicacion automatica.
  • EXISTS cuando necesitas columnas adicionales de la tabla principal que la segunda consulta no expone.
  • INNER JOIN cuando necesitas datos de ambas tablas, pero cuidado con la multiplicacion de filas y recurre a DISTINCT.

El manejo de NULL es la diferencia clave: INTERSECT considera NULL igual a NULL y empareja esas filas, mientras que un join sobre a.user_id = b.user_id descarta las filas cuya clave es NULL. INTERSECT brilla cuando ambos conjuntos provienen de expresiones complejas: en lugar de una subconsulta torpe en WHERE, escribes dos bloques SELECT legibles.

Soporte por motor

INTERSECT lleva tiempo disponible en PostgreSQL, Oracle, SQL Server y SQLite. En MySQL el operador llego solo en la version 8.0.31 — las versiones anteriores lo emulaban con un INNER JOIN mas DISTINCT, o con IN.

-- MySQL < 8.0.31 emulation of INTERSECT
SELECT DISTINCT user_id FROM purchases
WHERE user_id IN (SELECT user_id FROM refunds);

En ClickHouse, INTERSECT esta soportado, y que deduplique o conserve los duplicados lo gobierna el ajuste intersect_default_mode, no es algo fijo. Para no depender de ese ajuste, escribe INTERSECT DISTINCT para la semantica clasica de conjuntos o INTERSECT ALL cuando quieras conservar la multiplicidad.

Resumen rapido:

  • INTERSECT — interseccion de conjuntos con eliminacion de duplicados.
  • INTERSECT ALL — interseccion que respeta la multiplicidad: min(count_left, count_right) copias de cada fila.
  • Disponible en MySQL desde 8.0.31; en ClickHouse el comportamiento por defecto lo controla intersect_default_mode, asi que escribe DISTINCT o ALL de forma explicita.

INTERSECT hace que las consultas de "pertenencia comun" sean declarativas y legibles — usalo cuando compares conjuntos enteros de filas, y deja EXISTS/JOIN para los casos en los que necesitas columnas adicionales.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador