UNION, INTERSECT y EXCEPT en SQL
En esta lección
Un JOIN pone tablas una al lado de otra: añade columnas. Los operadores de conjuntos hacen algo distinto: ponen resultados uno debajo de otro, o se quedan con las filas comunes, o con las que sobran. Con UNION, INTERSECT y EXCEPT puedes responder preguntas como “dame todos mis contactos, sean clientes o proveedores” o “¿qué clientes no están suscritos al boletín?”.
Las tablas de esta lección
Además de clientes (ver la introducción), crearemos dos tablas nuevas: los proveedores de la tienda y los suscriptores del boletín (newsletter). Fíjate en que Ana y Pablo son clientes y también están suscritos, e Irene está suscrita pero no es cliente.
CREATE TABLE proveedores (
id INT PRIMARY KEY,
nombre VARCHAR(100) NOT NULL,
ciudad VARCHAR(50),
telefono VARCHAR(15)
);
INSERT INTO proveedores VALUES
(1, 'Textiles Norte', 'Bilbao', '944000111'),
(2, 'Cartonajes Pas', 'Torrelavega', '942000222'),
(3, 'Logística Meseta', 'Madrid', '910000333');
CREATE TABLE newsletter (
email VARCHAR(100) PRIMARY KEY,
nombre VARCHAR(100),
alta DATE
);
INSERT INTO newsletter VALUES
('ana@correo.es', 'Ana García', '2026-06-10'),
('pablo@correo.es', 'Pablo Díaz', '2026-07-02'),
('irene@correo.es', 'Irene Sanz', '2026-08-15');
UNION: juntar resultados
UNION coloca el resultado de la segunda consulta debajo del de la primera y elimina las filas repetidas. ¿En qué ciudades tenemos clientes o proveedores?
SELECT ciudad FROM clientes
UNION
SELECT ciudad FROM proveedores
ORDER BY ciudad;
| ciudad |
|---|
| Bilbao |
| Madrid |
| Santander |
| Torrelavega |
Hay cinco clientes y tres proveedores, pero salen solo cuatro ciudades: cada una aparece una vez.
UNION ALL: juntar sin quitar duplicados
UNION ALL hace lo mismo, pero conserva todas las filas, repetidas incluidas:
SELECT ciudad FROM clientes
UNION ALL
SELECT ciudad FROM proveedores;
| ciudad |
|---|
| Santander |
| Bilbao |
| Santander |
| Madrid |
| Madrid |
| Bilbao |
| Torrelavega |
| Madrid |
Ocho filas: cinco de clientes y tres de proveedores.
¿Cuál uso?
UNIONnecesita comparar todas las filas para quitar las repetidas. Con muchos datos es más lento.UNION ALLsolo las pega. Es más rápido.
Regla práctica: usa UNION ALL por defecto y pasa a UNION solo cuando quieras eliminar duplicados. Si sabes que no puede haber repetidos (por ejemplo, porque añades una columna que distingue el origen), UNION ALL da el mismo resultado sin trabajo extra.
Las reglas de UNION
Para que dos consultas se puedan unir:
- Deben tener el mismo número de columnas.
- Las columnas en la misma posición deben tener tipos compatibles (números con números, textos con textos).
- Los nombres de las columnas del resultado son los de la primera consulta.
ORDER BYva una sola vez, al final, y ordena el resultado completo.
Las columnas se emparejan por posición, no por nombre. La primera con la primera, la segunda con la segunda.
Ejemplo: una lista de contactos
Lista única de contactos con una columna que indica de dónde viene cada uno. La columna tipo es un texto fijo que escribimos nosotros:
SELECT nombre, ciudad, 'Cliente' AS tipo FROM clientes
UNION ALL
SELECT nombre, ciudad, 'Proveedor' FROM proveedores
ORDER BY ciudad, nombre;
| nombre | ciudad | tipo |
|---|---|---|
| Luis Pérez | Bilbao | Cliente |
| Textiles Norte | Bilbao | Proveedor |
| Eva Martín | Madrid | Cliente |
| Logística Meseta | Madrid | Proveedor |
| Pablo Díaz | Madrid | Cliente |
| Ana García | Santander | Cliente |
| Marta Ruiz | Santander | Cliente |
| Cartonajes Pas | Torrelavega | Proveedor |
Fíjate en que el alias AS tipo solo se escribe en la primera consulta: es la que da nombre a las columnas. Como las dos partes nunca coinciden (una dice “Cliente” y la otra “Proveedor”), aquí UNION ALL es la elección correcta.
El alias de la primera consulta también es el que usas en el ORDER BY:
SELECT nombre AS contacto FROM clientes WHERE ciudad = 'Madrid'
UNION
SELECT nombre FROM proveedores WHERE ciudad = 'Madrid'
ORDER BY contacto;
| contacto |
|---|
| Eva Martín |
| Logística Meseta |
| Pablo Díaz |
Ejemplo: una línea de tiempo
Con UNION ALL puedes mezclar sucesos de tablas distintas. Cuando una parte no tiene un dato, rellena con NULL:
SELECT fecha, 'Pedido' AS evento, importe FROM pedidos
UNION ALL
SELECT alta, 'Alta newsletter', NULL FROM newsletter
ORDER BY fecha;
| fecha | evento | importe |
|---|---|---|
| 2026-06-10 | Alta newsletter | NULL |
| 2026-07-02 | Alta newsletter | NULL |
| 2026-08-15 | Alta newsletter | NULL |
| 2026-09-01 | Pedido | 45.00 |
| 2026-09-03 | Pedido | 120.50 |
| 2026-09-10 | Pedido | 12.99 |
| 2026-09-12 | Pedido | 60.00 |
| 2026-09-15 | Pedido | 33.25 |
Ejemplo: un pequeño informe
Varias consultas de una sola fila unidas en una tabla:
SELECT 'Pedidos' AS concepto, COUNT(*) AS total FROM pedidos
UNION ALL
SELECT 'Clientes', COUNT(*) FROM clientes
UNION ALL
SELECT 'Proveedores', COUNT(*) FROM proveedores;
| concepto | total |
|---|---|
| Pedidos | 5 |
| Clientes | 5 |
| Proveedores | 3 |
INTERSECT: lo que está en las dos
INTERSECT devuelve las filas que aparecen en ambos resultados. ¿Qué clientes están suscritos al boletín?
SELECT nombre FROM clientes
INTERSECT
SELECT nombre FROM newsletter
ORDER BY nombre;
| nombre |
|---|
| Ana García |
| Pablo Díaz |
Y las ciudades donde hay a la vez clientes y proveedores:
SELECT ciudad FROM clientes
INTERSECT
SELECT ciudad FROM proveedores
ORDER BY ciudad;
| ciudad |
|---|
| Bilbao |
| Madrid |
EXCEPT: lo que está en la primera y no en la segunda
EXCEPT devuelve las filas de la primera consulta que no aparecen en la segunda. Aquí el orden importa: A EXCEPT B no es lo mismo que B EXCEPT A.
-- Clientes que NO están suscritos: candidatos para una campaña
SELECT nombre FROM clientes
EXCEPT
SELECT nombre FROM newsletter
ORDER BY nombre;
| nombre |
|---|
| Eva Martín |
| Luis Pérez |
| Marta Ruiz |
-- Suscriptores que todavía no son clientes
SELECT nombre FROM newsletter
EXCEPT
SELECT nombre FROM clientes;
| nombre |
|---|
| Irene Sanz |
Igual que UNION, tanto INTERSECT como EXCEPT eliminan duplicados. Algunos gestores tienen también INTERSECT ALL y EXCEPT ALL (PostgreSQL y MySQL), pero SQLite no.
Diferencias entre gestores
| Gestor | UNION | INTERSECT | EXCEPT |
|---|---|---|---|
| MySQL | Sí | Desde 8.0.31 | Desde 8.0.31 |
| MariaDB | Sí | Desde 10.3 | Desde 10.3 |
| PostgreSQL | Sí | Sí | Sí |
| SQLite | Sí | Sí | Sí |
| Oracle | Sí | Sí | Se llama MINUS (EXCEPT desde 21c) |
Si trabajas con un MySQL antiguo, puedes conseguir lo mismo con subconsultas: INTERSECT se parece a WHERE nombre IN (SELECT ...) y EXCEPT a WHERE NOT EXISTS (...). No son idénticos: IN no quita duplicados y la trampa del NULL en NOT IN sigue ahí.
¿UNION u OR?
Si las dos partes leen la misma tabla, a menudo basta con un OR:
-- Clientes de Madrid o menores de edad
SELECT nombre, ciudad FROM clientes
WHERE ciudad = 'Madrid' OR edad < 20;
| nombre | ciudad |
|---|---|
| Pablo Díaz | Madrid |
| Eva Martín | Madrid |
La versión con UNION ALL sacaría a Pablo dos veces, porque cumple las dos condiciones. Reserva UNION para cuando los datos vienen de tablas distintas o de consultas muy diferentes.
Errores frecuentes
- Distinto número de columnas.
SELECT nombre, ciudad FROM clientes
UNION
SELECT nombre FROM proveedores;
-- Error: las dos consultas no tienen el mismo número de columnas
ORDER BYen medio. Solo puede ir al final.SELECT ... ORDER BY nombre UNION SELECT ...da error. Si necesitas ordenar o limitar una parte por separado, métela entre paréntesis como subconsulta.- Columnas desordenadas. Si en la primera consulta pones
nombre, ciudady en la segundaciudad, nombre, no hay error (las dos son texto), pero las ciudades acaban en la columnanombre. Revisa el orden. - Tipos incompatibles. Unir
edad(número) contelefono(texto) funciona en MySQL y SQLite, que convierten los valores sin avisar, pero en PostgreSQL da error. Aunque funcione, mezclar tipos en una columna es mala señal. - Usar
UNIONsin querer quitar duplicados. Si dos clientes distintos se llaman igual y viven en la misma ciudad,UNIONlos fusionaría en uno. Cuando dudes,UNION ALL.
Resumen
| Operador | Devuelve | ¿Quita duplicados? |
|---|---|---|
A UNION B | Filas de A y de B | Sí |
A UNION ALL B | Filas de A y de B | No (más rápido) |
A INTERSECT B | Filas que están en A y en B | Sí |
A EXCEPT B | Filas de A que no están en B | Sí |
- Mismo número de columnas, tipos compatibles, emparejadas por posición.
- Los nombres de las columnas salen de la primera consulta.
- Un único
ORDER BY, al final del todo. FULL OUTER JOINse emula en MySQL conUNION ALL: lo tienes en más JOIN.
En la siguiente lección, INSERT, UPDATE y DELETE, dejarás de solo leer datos y empezarás a modificarlos.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.