PostgreSQL: Operações de Conjunto e Consultas Combinadas no…
Última atualização: 2026-08-26
1. O Que Você Vai Aprender
- UNION (mesclagem com desduplicação)
- UNION ALL (mesclagem preservando duplicatas)
- INTERSECT / INTERSECT ALL (interseção)
- EXCEPT / EXCEPT ALL (diferença)
- ORDER BY em operações de conjunto
- Tratamento de NULL em operações de conjunto
- Escolha entre operações de conjunto e JOIN
2. A História
Bob é um analista de dados em uma plataforma de e-commerce. O CEO faz três perguntas a ele:
- Quais produtos foram mais vendidos no T1, T2 e T3? (UNION ALL para mesclar)
- Quais produtos foram mais vendidos em todos os trimestres? (INTERSECT = perenes)
- Quais produtos venderam bem no T1 mas não no T2? (EXCEPT = perdidos)
Bob percebe que essas perguntas se mapeiam exatamente para as três operações de conjunto do SQL: UNION, INTERSECT, EXCEPT.
3. Conceito
(1) Visão Geral das Operações de Conjunto
| Operação | Significado | Desduplica | Analogia |
|---|---|---|---|
| UNION | Mesclar conjuntos de resultados | Sim | A ∪ B |
| UNION ALL | Mesclar conjuntos de resultados | Não | A ∪ B (com duplicatas) |
| INTERSECT | Interseção | Sim | A ∩ B |
| INTERSECT ALL | Interseção | Não | A ∩ B (com contagens de duplicatas) |
| EXCEPT | Em A mas não em B | Sim | A - B |
| EXCEPT ALL | Em A mas não em B | Não | A - B (com contagens de duplicatas) |
flowchart TD
subgraph Union
U1((A)) --- U2((B))
U1 & U2 --> U3["A ∪ B"]
end
subgraph Intersect
I1((A)) --- I2((B))
I1 ∩ I2 --> I3["A ∩ B"]
end
subgraph Except
E1((A)) --- E2((B))
E1 - E2 --> E3["A - B"]
end
style U3 fill:#c8e6c9
style I3 fill:#e1f5fe
style E3 fill:#fff9c4
(2) UNION e UNION ALL
▶ Exemplo: UNION Mescla Mais Vendidos de Três Trimestres
SELECT product_id, product_name FROM hot_products_q1
UNION
SELECT product_id, product_name FROM hot_products_q2
UNION
SELECT product_id, product_name FROM hot_products_q3
ORDER BY product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
UNION desduplica automaticamente: se um produto é mais vendido tanto no T1 quanto no T2, ele aparece apenas uma vez.
▶ Exemplo: UNION ALL Preserva Duplicatas
SELECT product_id, product_name, 'Q1' AS quarter FROM hot_products_q1
UNION ALL
SELECT product_id, product_name, 'Q2' AS quarter FROM hot_products_q2
UNION ALL
SELECT product_id, product_name, 'Q3' AS quarter FROM hot_products_q3
ORDER BY quarter, product_id;
product_id | product_name | quarter
------------+--------------+---------
101 | Widget Pro | Q1
102 | Gadget Mini | Q1
101 | Widget Pro | Q2
103 | Server Rack | Q2
101 | Widget Pro | Q3
104 | Cable Max | Q3
| Cenário | Recomendado | Motivo |
|---|---|---|
| Mesclar fontes diferentes sem duplicatas | UNION ALL | Sem necessidade de desduplicação, mais rápido |
| Mesclar com possíveis duplicatas que devem ser removidas | UNION | Desduplica automaticamente |
| Mesclar e identificar a fonte | UNION ALL + coluna de identificação | Mantém duplicatas e distingue fontes |
▶ Exemplo: UNION ALL para Mesclar Estatísticas de Múltiplas Tabelas
SELECT 'NA' AS region, COUNT(*) AS order_count, SUM(amount) AS total FROM orders_na
UNION ALL
SELECT 'EU', COUNT(*), SUM(amount) FROM orders_eu
UNION ALL
SELECT 'APAC', COUNT(*), SUM(amount) FROM orders_apac;
Output:
count
-------
5
(1 row)
(3) INTERSECT e INTERSECT ALL
▶ Exemplo: Encontrar Produtos Perenes que Venderam Bem em Todos os Três Trimestres
SELECT product_id, product_name FROM hot_products_q1
INTERSECT
SELECT product_id, product_name FROM hot_products_q2
INTERSECT
SELECT product_id, product_name FROM hot_products_q3;
product_id | product_name
------------+--------------
101 | Widget Pro
Widget Pro é o único produto perene que foi mais vendido em todos os trimestres.
▶ Exemplo: INTERSECT ALL Preserva Contagens de Duplicatas
Suponha que o produto 101 apareça 2 vezes na lista de mais vendidos do T1, 1 vez no T2 e 3 vezes no T3:
SELECT product_id FROM hot_products_q1_detail
INTERSECT ALL
SELECT product_id FROM hot_products_q2_detail;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
INTERSECT ALL retorna MIN(contagem de ocorrências): o produto 101 retorna min(2, 1) = 1 linha.
| Operação | Desduplica | Tratamento de linhas duplicadas | Uso típico |
|---|---|---|---|
| INTERSECT | Sim | Mantém apenas uma linha | Encontrar itens comuns |
| INTERSECT ALL | Não | Mantém pela contagem mínima de ocorrências | Corresponder frequência de duplicatas exatamente |
▶ Exemplo: INTERSECT Encontra Clientes Comuns a Múltiplos Anos
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2023
INTERSECT
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2024
INTERSECT
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2025;
Output:
CREATE TABLE
Clientes fiéis que fizeram pedidos em todos os três anos consecutivos.
(4) EXCEPT e EXCEPT ALL
▶ Exemplo: Mais Vendidos do T1 que Não Apareceram no T2 (Perdidos)
SELECT product_id, product_name FROM hot_products_q1
EXCEPT
SELECT product_id, product_name FROM hot_products_q2;
product_id | product_name
------------+--------------
102 | Gadget Mini
Gadget Mini foi mais vendido no T1 mas não apareceu na lista do T2—ele foi perdido.
▶ Exemplo: Novos Mais Vendidos Promovidos no T2
SELECT product_id, product_name FROM hot_products_q2
EXCEPT
SELECT product_id, product_name FROM hot_products_q1;
product_id | product_name
------------+--------------
103 | Server Rack
Server Rack entrou recentemente na lista de mais vendidos no T2.
▶ Exemplo: EXCEPT ALL Preserva Contagens de Duplicatas
SELECT product_id FROM order_items_2024
EXCEPT ALL
SELECT product_id FROM order_items_2025;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
Se o produto 101 aparecer 5 vezes em 2024 e 3 vezes em 2025, EXCEPT ALL retorna 5 - 3 = 2 linhas.
| Operação | Desduplica | Tratamento de linhas duplicadas | Uso típico |
|---|---|---|---|
| EXCEPT | Sim | Mantém apenas uma linha | Encontrar diferenças |
| EXCEPT ALL | Não | Mantém pela diferença de contagem de ocorrências | Calcular contagem exata de excedente |
4. Pontos-Chave
(1) Regras para Operações de Conjunto
Regra um: o número de colunas deve ser o mesmo.
SELECT id, name FROM table_a
UNION
SELECT id, name, price FROM table_b;
ERRO: cada consulta UNION deve ter o mesmo número de colunas
Regra dois: os tipos de coluna correspondentes devem ser compatíveis.
| Regra | Requisito | Consequência da violação |
|---|---|---|
| Contagem de colunas | Deve ser a mesma | Erro de compilação |
| Tipo de coluna | Deve ser compatível | Conversão implícita ou erro |
| Nome da coluna | Usa o nome da coluna da primeira consulta | Cuidado com aliases |
▶ Exemplo: Unificar Nomes de Coluna com Aliases
SELECT product_id, product_name AS name FROM products_active
UNION ALL
SELECT sku AS product_id, title AS name FROM products_legacy
ORDER BY product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) Posição do ORDER BY em Operações de Conjunto
ORDER BY pode aparecer apenas após a última consulta e aplica-se a todo o conjunto de resultados.
▶ Exemplo: ORDER BY Correto
SELECT product_id, product_name FROM hot_products_q1
UNION ALL
SELECT product_id, product_name FROM hot_products_q2
ORDER BY product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Controlar Precedência com Parênteses
(SELECT product_id FROM hot_products_q1
EXCEPT
SELECT product_id FROM hot_products_q2)
UNION ALL
(SELECT product_id FROM hot_products_q2
EXCEPT
SELECT product_id FROM hot_products_q3)
ORDER BY product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
Calcule a diferença primeiro, depois mescle. Sem parênteses, UNION tem precedência menor que INTERSECT/EXCEPT.
| Operação | Precedência | Associatividade |
|---|---|---|
| INTERSECT | Mais alta | Esquerda para direita |
| EXCEPT | Média | Esquerda para direita |
| UNION / UNION ALL | Média | Esquerda para direita |
(3) Tratamento de NULL em Operações de Conjunto
Operações de conjunto tratam NULL como igual (diferente de comparações comuns onde NULL <> NULL).
▶ Exemplo: Desduplicação de NULL no UNION
SELECT NULL AS val
UNION
SELECT NULL AS val;
val
-----
(1 row)
Os dois NULLs são tratados como iguais; UNION desduplica para uma única linha.
▶ Exemplo: Correspondência de NULL no INTERSECT
SELECT NULL AS val
INTERSECT
SELECT NULL AS val;
val
-----
(1 row)
NULL corresponde a NULL; INTERSECT retorna uma linha.
| Cenário | Comportamento do NULL | Diferença da comparação comum |
|---|---|---|
| UNION | Dois NULLs tratados como iguais, desduplicados | NULL = NULL comum é UNKNOWN |
| INTERSECT | Dois NULLs tratados como iguais, correspondidos | NULL = NULL comum é UNKNOWN |
| EXCEPT | Dois NULLs tratados como iguais, cancelados | NULL <> NULL comum é UNKNOWN |
(4) Operações de Conjunto vs JOIN
▶ Exemplo: INTERSECT Equivalente a INNER JOIN
SELECT a.product_id
FROM hot_products_q1 a
INNER JOIN hot_products_q2 b ON a.product_id = b.product_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
Equivalente a:
SELECT product_id FROM hot_products_q1
INTERSECT
SELECT product_id FROM hot_products_q2;
| Dimensão | Operação de conjunto | JOIN |
|---|---|---|
| Semântica | Operação de conjunto sobre linhas | Combinação de colunas |
| Colunas de saída | Usa as colunas do lado esquerdo | Colunas de ambas as tabelas disponíveis |
| Desduplicação | UNION/INTERSECT/EXCEPT desduplicam automaticamente | Precisa de DISTINCT manual |
| Correspondência NULL | NULL = NULL | NULL <> NULL |
| Desempenho | Dados grandes podem ordenar para desduplicar | Hash Join indexado pode ser mais rápido |
| Melhor para | Mesclar/interceptar/diferenciar conjuntos de resultados com mesma estrutura | Unir tabelas heterogêneas para buscar colunas |
5. Prática
▶ Exemplo: UNION ALL para Mesclar Pedidos e Livro de Reembolsos
SELECT
order_id AS transaction_id,
amount AS credit,
0 AS debit,
'order' AS type,
created_at
FROM orders
UNION ALL
SELECT
refund_id,
0,
refund_amount,
'refund',
created_at
FROM refunds
ORDER BY created_at;
Output:
CREATE TABLE
▶ Exemplo: EXCEPT Encontra Usuários Registrados mas Não Ativados
SELECT user_id, email FROM registered_users
EXCEPT
SELECT user_id, email FROM activated_users;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: INTERSECT Encontra Clientes que Compraram Ambos A e B
SELECT customer_id FROM order_items WHERE product_id = 101
INTERSECT
SELECT customer_id FROM order_items WHERE product_id = 102;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Análise de Retenção de Clientes em Três Anos
SELECT 'retained' AS status, COUNT(*) AS cnt FROM (
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2024
INTERSECT
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2025
) t
UNION ALL
SELECT 'churned', COUNT(*) FROM (
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2024
EXCEPT
SELECT customer_id FROM orders WHERE EXTRACT(YEAR FROM created_at) = 2025
) t;
Output:
count
-------
5
(1 row)
▶ Exemplo: UNION ALL + GROUP BY para Resumo de Tendências
SELECT
product_id,
SUM(CASE WHEN quarter = 'Q1' THEN 1 ELSE 0 END) AS q1_count,
SUM(CASE WHEN quarter = 'Q2' THEN 1 ELSE 0 END) AS q2_count,
SUM(CASE WHEN quarter = 'Q3' THEN 1 ELSE 0 END) AS q3_count
FROM (
SELECT product_id, 'Q1' AS quarter FROM hot_products_q1
UNION ALL
SELECT product_id, 'Q2' FROM hot_products_q2
UNION ALL
SELECT product_id, 'Q3' FROM hot_products_q3
) combined
GROUP BY product_id
ORDER BY product_id;
Output:
count
-------
5
(1 row)
6. Exemplo Completo
Análise de mais vendidos entre trimestres de Bob—uma saída para produtos perenes, novos e perdidos:
WITH q1 AS (
SELECT product_id, product_name FROM hot_products WHERE quarter = 'Q1'
),
q2 AS (
SELECT product_id, product_name FROM hot_products WHERE quarter = 'Q2'
),
q3 AS (
SELECT product_id, product_name FROM hot_products WHERE quarter = 'Q3'
),
evergreen AS (
SELECT product_id, product_name, 'evergreen' AS trend FROM q1
INTERSECT
SELECT product_id, product_name, 'evergreen' FROM q2
INTERSECT
SELECT product_id, product_name, 'evergreen' FROM q3
),
new_q2 AS (
SELECT product_id, product_name, 'new_in_q2' AS trend FROM q2
EXCEPT
SELECT product_id, product_name, 'new_in_q2' FROM q1
),
new_q3 AS (
SELECT product_id, product_name, 'new_in_q3' AS trend FROM q3
EXCEPT
SELECT product_id, product_name, 'new_in_q3' FROM q2
),
churned_q2 AS (
SELECT product_id, product_name, 'churned_in_q2' AS trend FROM q1
EXCEPT
SELECT product_id, product_name, 'churned_in_q2' FROM q2
),
churned_q3 AS (
SELECT product_id, product_name, 'churned_in_q3' AS trend FROM q2
EXCEPT
SELECT product_id, product_name, 'churned_in_q3' FROM q3
)
SELECT * FROM evergreen
UNION ALL
SELECT * FROM new_q2
UNION ALL
SELECT * FROM new_q3
UNION ALL
SELECT * FROM churned_q2
UNION ALL
SELECT * FROM churned_q3
ORDER BY trend, product_id;
product_id | product_name | trend
------------+--------------+---------------
101 | Widget Pro | evergreen
103 | Server Rack | new_in_q2
104 | Cable Max | new_in_q3
102 | Gadget Mini | churned_in_q2
103 | Server Rack | churned_in_q3
7. Fluxo de Execução das Operações de Conjunto
flowchart TD
A["Consulta A"] --> C{Operação}
B["Consulta B"] --> C
C -->|UNION| D["Combinar + Desduplicar"]
C -->|UNION ALL| E["Combinar (manter duplicatas)"]
C -->|INTERSECT| F["Corresponder + Desduplicar"]
C -->|EXCEPT| G["A - B + Desduplicar"]
D --> H["ORDER BY (opcional)"]
E --> H
F --> H
G --> H
H --> I["Resultado Final"]
style D fill:#c8e6c9
style F fill:#e1f5fe
style G fill:#fff9c4
| Passo | Operação | Descrição |
|---|---|---|
| 1 | Executar cada subconsulta | Executadas independentemente; conjuntos de resultados devem compartilhar estrutura |
| 2 | Operação de conjunto | UNION/INTERSECT/EXCEPT |
| 3 | Desduplicar (se necessário) | UNION/INTERSECT/EXCEPT desduplicam por padrão |
| 4 | ORDER BY | Aplica-se ao conjunto de resultados final |
| 5 | LIMIT | Limitar linhas de saída final |
❓ Perguntas Frequentes
P: Qual é mais rápido, UNION ou UNION ALL? R: UNION ALL é mais rápido porque não desduplica. Se você tem certeza de que não há duplicatas, ou não precisa de desduplicação, prefira UNION ALL.
P: De quem é o nome da coluna que uma operação de conjunto usa? R: Usa o nome da coluna (ou alias) da primeira consulta. Para unificá-los, escreva o alias na primeira consulta.
P: Uma operação de conjunto pode ser usada em uma subconsulta? R: Sim.
SELECT * FROM (A UNION B) AS té válido; coloque entre parênteses e adicione um alias.
P: Qual a precedência de múltiplas operações de conjunto? R: INTERSECT > (EXCEPT = UNION). INTERSECT tem a precedência mais alta; EXCEPT e UNION estão no mesmo nível. Use parênteses para tornar a precedência explícita.
P: NULL é realmente igual a NULL em operações de conjunto? R: Sim. Em operações de conjunto, dois NULLs são tratados como iguais—este é o comportamento SQL padrão, diferente de comparações comuns onde NULL = NULL é UNKNOWN.
P: Quando usar uma operação de conjunto em vez de JOIN? R: Use uma operação de conjunto quando você só precisa testar "existe / não existe" e não precisa das colunas unidas; use JOIN quando precisa combinar colunas de ambas as tabelas. Operações de conjunto são mais intuitivas de ler; JOIN é mais flexível.
📖 Resumo
- UNION mescla e desduplica; UNION ALL mescla e mantém duplicatas (melhor desempenho)
- INTERSECT obtém a interseção; EXCEPT obtém a diferença
- O sufixo ALL mantém contagens de duplicatas: INTERSECT ALL / EXCEPT ALL
- Operações de conjunto exigem o mesmo número de colunas e tipos compatíveis
- ORDER BY pode aparecer apenas no final da instrução
- NULL é tratado como igual em operações de conjunto (diferente de comparações comuns)
- Precedência: INTERSECT = EXCEPT > UNION; use parênteses para controlá-la
- Operações de conjunto são adequadas para mesclar/interceptar/diferenciar conjuntos de resultados com mesma estrutura; JOIN é adequado para unir tabelas heterogêneas
📝 Exercícios
- ⭐ Use UNION ALL para mesclar as tabelas de pedidos de 2024 e 2025, adicionando uma coluna de ano, ordenado por valor decrescente.
- ⭐ Use EXCEPT para encontrar usuários que estão na tabela customers mas não na tabela active_users.
- ⭐⭐ Use INTERSECT para encontrar IDs de produtos mais vendidos que aparecem em todos os três trimestres, e faça JOIN com a tabela products para exibir os nomes dos produtos.
- ⭐⭐ Use uma CTE + EXCEPT para análise de perda de clientes: clientes que tiveram pedidos em 2024 mas nenhum em 2025.
- ⭐⭐⭐ Escreva uma única instrução SQL combinando UNION ALL + INTERSECT + EXCEPT: produza um relatório cobrindo três categorias de produtos (perene / novo / perdido), cada uma com uma coluna de identificação, e finalmente use GROUP BY para contar os produtos em cada categoria.