1. Qué es
Un JOIN pega tablas de lado: añade columnas. Los operadores de conjuntos las pegan de arriba abajo: añaden filas.
JOIN: [A][B] → más columnas
UNION: [A]
[B] → más filas
| Operador | Devuelve |
|---|---|
UNION |
Filas de A y de B, sin repetidos |
UNION ALL |
Filas de A y de B, tal cual |
INTERSECT |
Solo las que están en las dos |
EXCEPT |
Las de A que no están en B |
INTERSECT y EXCEPT también eliminan duplicados salvo que añadas ALL.
2. Las reglas
Las tres son innegociables y las tres se olvidan:
- Mismo número de columnas en todas las ramas.
- Tipos compatibles, columna por columna, en el mismo orden.
- Los nombres los pone la primera rama. Los alias de las demás se ignoran.
Y dos de sintaxis:
- El
ORDER BYva al final, una sola vez, y afecta al resultado completo. - Se evalúan de arriba abajo, con
INTERSECTantes queUNIONyEXCEPT. Con tres ramas o más, usa paréntesis.
SELECT a, b FROM t1
UNION ALL
SELECT a, b FROM t2
ORDER BY a;
3. Ejemplos sobre Northwind
UNION contra UNION ALL — la diferencia importa:
SELECT count(*) FROM (SELECT country FROM customers UNION SELECT country FROM suppliers) t; -- 25
SELECT count(*) FROM (SELECT country FROM customers UNION ALL SELECT country FROM suppliers) t; -- 120
25 contra 120. UNION ALL apila las 91 filas de clientes y las 29 de proveedores. UNION deduplica el resultado entero y deja los 25 países distintos.
Y hay un detalle que se pasa por alto: UNION no solo quita los duplicados entre ramas, también los de dentro de cada rama:
SELECT count(*) FROM (SELECT customer_id FROM orders UNION SELECT customer_id FROM orders) t; -- 89
SELECT count(*) FROM (SELECT customer_id FROM orders UNION ALL SELECT customer_id FROM orders) t; -- 1660
La misma consulta unida consigo misma da 89, no 1.660. UNION es UNION ALL + DISTINCT sobre todo.
Eso se ve en el plan:
-- UNION -- UNION ALL
HashAggregate Append
Group Key: country -> Seq Scan on customers
-> Append -> Seq Scan on suppliers
-> Seq Scan on customers
-> Seq Scan on suppliers
UNION ALL es solo Append: pegar y ya. UNION añade un HashAggregate encima, que cuesta memoria y tiempo.
Usa
UNION ALLpor defecto.UNIONsolo cuando de verdad puede haber duplicados y te molestan. Es al revés de lo que hace todo el mundo: se escribeUNIONpor costumbre y se paga una deduplicación que nadie pidió.
3.1 El uso más común: etiquetar el origen
SELECT company_name, city, 'cliente' AS tipo FROM customers WHERE city = 'London'
UNION ALL
SELECT company_name, city, 'proveedor' FROM suppliers WHERE city = 'London'
ORDER BY tipo, company_name;
company_name | city | tipo
-----------------------+--------+-----------
Around the Horn | London | cliente
B's Beverages | London | cliente
Consolidated Holdings | London | cliente
Eastern Connection | London | cliente
North/South | London | cliente
Seven Seas Imports | London | cliente
Exotic Liquids | London | proveedor
La columna tipo es literal, inventada en el SELECT, y es lo que hace útil el resultado: sin ella, siete empresas de Londres sin saber cuál es cuál. Cuando unas dos tablas distintas, etiqueta siempre el origen.
Fíjate también en que la segunda rama no lleva alias: sería inútil. Los nombres de columna los fija la primera.
3.2 INTERSECT — lo que está en los dos sitios
SELECT count(*) FROM (SELECT country FROM customers INTERSECT SELECT country FROM suppliers) t; -- 12
12 países donde hay clientes y proveedores. Es la misma pregunta que el FULL JOIN de left-right-y-full-join, respondida en dos líneas — porque aquí solo quiero la columna que comparo, no columnas de los dos lados.
3.3 EXCEPT — lo que está en uno y no en el otro
SELECT country FROM customers EXCEPT SELECT country FROM suppliers ORDER BY 1;
country
-------------
Argentina
Austria
Belgium
Ireland
Mexico
Poland
Portugal
Switzerland
Venezuela
9 países donde vendemos y no compramos. Y al revés:
SELECT country FROM suppliers EXCEPT SELECT country FROM customers ORDER BY 1;
Australia
Japan
Netherlands
Singapore
4 países donde compramos y no vendemos. 12 + 9 + 4 = 25. ✓
EXCEPTno es simétrico.A EXCEPT ByB EXCEPT Ason preguntas distintas y hay que hacer las dos. Es el error de lectura más común del operador.
3.4 Los nulos aquí sí son iguales
SELECT region FROM customers INTERSECT SELECT region FROM suppliers ORDER BY 1 NULLS LAST;
region
--------
OR
Québec
Tres filas: OR, Québec y una vacía. Esa última es NULL, y está ahí porque las dos tablas tienen regiones nulas.
Los operadores de conjuntos tratan NULL = NULL como cierto, igual que GROUP BY y DISTINCT, y al revés que = en un WHERE o en un ON (null-y-la-logica-de-tres-valores). Es coherente —para comparar filas, dos ausencias son la misma ausencia— pero hay que saberlo: un EXCEPT puede eliminar una fila por coincidir en un NULL.
3.5 Añadir una fila de total
SELECT extract(year FROM order_date)::int AS anio, count(*) FROM orders GROUP BY 1
UNION ALL
SELECT NULL, count(*) FROM orders
ORDER BY 1 NULLS LAST;
anio | count
------+-------
1996 | 152
1997 | 408
1998 | 270
| 830
Funciona, y es exactamente lo que ROLLUP hace mejor (agregacion-avanzada): aquí la tabla orders se lee dos veces; con ROLLUP, una. Cuando veas un UNION ALL con un total pegado abajo, casi siempre es un ROLLUP sin escribir.
4. Errores comunes
-
Distinto número de columnas.
sql SELECT country, city FROM customers UNION SELECT country FROM suppliers;ERROR: each UNION query must have the same number of columnsEste avisa. -
Columnas en distinto orden — este NO avisa.
sql SELECT country FROM customers UNION SELECT company_name FROM suppliers;Devuelve 50 filas mezclando países y nombres de empresa bajo el títulocountry. Los dos sonvarchar, así que PostgreSQL no tiene nada que objetar. El motor comprueba los tipos, no el significado.
Cuando los tipos sí chocan, sí avisa:
sql
SELECT 1 UNION SELECT 'a';
ERROR: invalid input syntax for type integer: "a"
-
ORDER BYen una rama intermedia.sql SELECT country FROM customers ORDER BY country UNION SELECT country FROM suppliers;ERROR: syntax error at or near "UNION"Va al final, una sola vez. -
UNIONpor costumbre cuando quieresUNION ALL. Deduplicación silenciosa: si dos filas legítimas eran idénticas, pierdes una. En unUNIONde importes de dos sucursales, dos ventas del mismo importe se convierten en una. -
Esperar que
EXCEPTsea simétrico. -
Ordenar por una columna que no está en el
SELECT. En unUNIONno se puede: elORDER BYsolo ve las columnas del resultado. -
Usar
UNIONpara lo que es unJOIN. Si necesitas columnas de las dos tablas en la misma fila, es unJOIN.UNIONapila; no cruza.
5. Diferencias con SQL Server
| PostgreSQL | SQL Server | |
|---|---|---|
UNION / UNION ALL |
✅ | ✅ Idéntico |
INTERSECT / EXCEPT |
✅ | ✅ Idéntico |
INTERSECT ALL / EXCEPT ALL |
✅ | ❌ No existen |
MINUS (nombre alternativo de EXCEPT) |
❌ | ❌ (es de Oracle) |
INTERSECT ALL y EXCEPT ALL son la única diferencia, y conservan las repeticiones contando ocurrencias: si A tiene un valor 3 veces y B 1, EXCEPT ALL lo devuelve 2 veces. Se usan poco; si el código tiene que valer para los dos motores, evítalos.
Si te encuentras MINUS, es Oracle: se traduce a EXCEPT tal cual.
6. Ejercicios
- Una lista única de todas las ciudades donde hay clientes o proveedores.
- Un directorio de contactos: nombre de contacto, empresa, teléfono y tipo (
cliente/proveedor), ordenado por empresa. - Las ciudades que tienen clientes pero ningún proveedor.
- Los ids de producto que aparecen en
productspero nunca enorder_details, resuelto con un operador de conjuntos, y luego conNOT EXISTS. ¿Cuál prefieres? - Explica por qué esta consulta devuelve menos filas de las esperadas y cómo lo arreglarías:
sql SELECT freight FROM orders WHERE ship_country = 'Brazil' UNION SELECT freight FROM orders WHERE ship_country = 'France';
7. Pregunta de negocio
Administración quiere un único listado de "todas las empresas con las que trabajamos" —clientes y proveedores— con nombre, contacto, teléfono, ciudad, país y de qué tipo es cada una. Y avisan de que hay empresas que podrían ser las dos cosas.