PostgreSQL: Consultas de Junção de Múltiplas Tabelas no…
Última atualização: 2026-08-26
1. O Que Você Vai Aprender
- INNER JOIN (junção interna)
- LEFT / RIGHT / FULL OUTER JOIN (junções externas)
- CROSS JOIN (junção cruzada)
- NATURAL JOIN (use com cautela)
- Auto-junção
- Sintaxe abreviada USING
- Junções de múltiplas tabelas (3+ tabelas)
- Recurso PostgreSQL: LATERAL JOIN
- Noções básicas de desempenho de JOIN (introdução ao EXPLAIN)
2. A História
Charlie é engenheiro de back-end em uma plataforma SaaS. O gerente de produto pede que ele gere um relatório detalhado de compras de usuários que precisa combinar 4 tabelas:
- users — informações do usuário
- orders — pedidos
- order_items — itens do pedido
- products — produtos
Um usuário pode ter vários pedidos; cada pedido tem vários itens; cada item mapeia para um produto. Charlie precisa escolher o tipo de JOIN correto para garantir que nenhum usuário sem pedido seja descartado e que nenhum produto cartesiano inesperado seja criado.
3. Conceito
(1) Visão Geral dos Tipos de JOIN
| Tipo de JOIN | Significado | Qual lado é mantido | Cenário típico |
|---|---|---|---|
| INNER JOIN | Mantém apenas linhas correspondentes | Nenhum lado | Exigir correspondência para incluir |
| LEFT JOIN | Mantém todas da tabela esquerda | Esquerda | Tabela principal + linhas relacionadas opcionais |
| RIGHT JOIN | Mantém todas da tabela direita | Direita | Raramente usado |
| FULL JOIN | Mantém todas de ambos os lados | Ambos | Encontrar diferenças / reconciliar |
| CROSS JOIN | Produto cartesiano | Incondicional | Permutações / combinações |
| LATERAL JOIN | Subconsulta referencia a tabela esquerda | — | Top-N por linha |
flowchart LR
subgraph Interna
A1((A)) --- B1((B))
end
subgraph Esquerda
A2((A)) --- B2((B))
A3((A)) -.-> B3((∅))
end
subgraph Completa
A4((A)) --- B4((B))
A5((A)) -.-> B6((∅))
B5((∅)) -.-> A6((A))
end
style A1 fill:#c8e6c9
style B1 fill:#c8e6c9
style A2 fill:#c8e6c9
style A3 fill:#c8e6c9
style B2 fill:#c8e6c9
style A4 fill:#c8e6c9
style A5 fill:#c8e6c9
style B4 fill:#c8e6c9
(2) INNER JOIN
Retorna apenas as linhas que correspondem em ambas as tabelas.
▶ Exemplo: Consultar Usuários que Têm Pedidos
SELECT
u.user_id,
u.name,
o.order_id,
o.amount
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id;
user_id | name | order_id | amount
---------+-------+----------+--------
1 | Alice | 101 | 15000
1 | Alice | 102 | 8000
2 | Bob | 201 | 25000
Usuários que nunca fizeram um pedido não aparecem.
(3) LEFT JOIN
Mantém todas as linhas da tabela esquerda; preenche com NULL na direita quando não há correspondência.
▶ Exemplo: Incluir Usuários Sem Pedidos
SELECT
u.user_id,
u.name,
o.order_id,
o.amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id;
user_id | name | order_id | amount
---------+---------+----------+--------
1 | Alice | 101 | 15000
1 | Alice | 102 | 8000
2 | Bob | 201 | 25000
3 | Charlie | |
Charlie não tem pedidos, então order_id e amount são NULL.
▶ Exemplo: Encontrar Usuários Sem Pedidos
SELECT u.user_id, u.name
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(4) RIGHT JOIN e FULL JOIN
▶ Exemplo: FULL JOIN para Encontrar Diferenças Entre Usuários e Pedidos
SELECT
u.user_id,
u.name,
o.order_id,
o.user_id AS order_user_id
FROM users u
FULL JOIN orders o ON u.user_id = o.user_id
ORDER BY u.user_id NULLS LAST, o.order_id NULLS LAST;
user_id | name | order_id | order_user_id
---------+---------+----------+---------------
1 | Alice | 101 | 1
2 | Bob | 201 | 2
3 | Charlie | |
| | 999 | 99
O pedido com user_id=99 não tem usuário correspondente, e o usuário com user_id=3 não tem pedido — ambas as diferenças são visíveis.
▶ Exemplo: RIGHT JOIN para Visualizar Pedidos Órfãos
SELECT o.order_id, o.user_id, u.name
FROM users u
RIGHT JOIN orders o ON u.user_id = o.user_id
WHERE u.user_id IS NULL;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| Tipo de JOIN | Comportamento | Comparação de uso |
|---|---|---|
| LEFT JOIN | Mantém todas da tabela esquerda | Consultar principal + relacionado; encontrar lacunas à esquerda |
| RIGHT JOIN | Mantém todas da tabela direita | Pode ser reescrito como LEFT JOIN com lados trocados |
| FULL JOIN | Mantém todas de ambos os lados | Reconciliação, encontrar diferenças |
(5) CROSS JOIN e NATURAL JOIN
▶ Exemplo: CROSS JOIN Gera Todas as Combinações
SELECT
d.department_name,
p.project_name
FROM departments d
CROSS JOIN projects p;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
3 departamentos × 5 projetos = 15 linhas.
▶ Exemplo: NATURAL JOIN (use com cautela)
SELECT * FROM users
NATURAL JOIN orders;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
O NATURAL JOIN faz a junção automaticamente nas colunas com o mesmo nome em ambas as tabelas. Perigoso: se as duas tabelas compartilham várias colunas com nomes idênticos (ex.: created_at), ele produz silenciosamente condições AND extras. Prefira um ON ou USING explícito.
| Sintaxe | Vantagens | Desvantagens |
|---|---|---|
| NATURAL JOIN | Conciso | Uma alteração de nome de coluna pode mudar a lógica de junção |
| USING(col) | Conciso e explícito | Apenas para equi-junções em colunas com mesmo nome |
| ON a.col = b.col | Controle total | Verboso |
(6) Sintaxe USING
▶ Exemplo: USING no Lugar de ON
SELECT
u.name,
o.order_id,
o.amount
FROM users u
JOIN orders o USING (user_id);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
USING mescla colunas com o mesmo nome em uma só; SELECT * não exibe user_id duas vezes.
(7) Auto-Junção
Uma tabela unida consigo mesma, útil para hierarquias ou comparações na mesma tabela.
▶ Exemplo: Emparelhamento Funcionário–Gerente
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;
employee | manager
----------+---------
Alice | David
Bob | Alice
Charlie | Alice
David |
▶ Exemplo: Comparar Preços de Produtos na Mesma Categoria
SELECT
a.product_name AS product_a,
b.product_name AS product_b,
a.price - b.price AS price_diff
FROM products a
JOIN products b ON a.category_id = b.category_id
AND a.product_id < b.product_id
AND ABS(a.price - b.price) < 100;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
4. Pontos-Chave
(1) Junções de Múltiplas Tabelas (3+ Tabelas)
Requisito de Charlie: unir users → orders → order_items → products.
▶ Exemplo: Detalhe de Compra de Usuário com 4 Tabelas
SELECT
u.name AS user_name,
o.order_id,
o.created_at AS order_date,
p.product_name,
oi.quantity,
oi.unit_price,
oi.quantity * oi.unit_price AS line_total
FROM users u
JOIN orders o ON u.user_id = o.user_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
ORDER BY u.name, o.order_id, oi.order_item_id;
user_name | order_id | order_date | product_name | quantity | unit_price | line_total
-----------+----------+----------------------+--------------+----------+------------+------------
Alice | 101 | 2025-03-15 10:30:00 | Widget Pro | 2 | 15000 | 30000
Alice | 101 | 2025-03-15 10:30:00 | Gadget Mini | 5 | 3000 | 15000
Alice | 102 | 2025-04-02 14:20:00 | Widget Pro | 1 | 15000 | 15000
Bob | 201 | 2025-05-10 09:00:00 | Server Rack | 1 | 80000 | 80000
(2) LATERAL JOIN (Recurso PostgreSQL)
LATERAL permite que uma subconsulta referencie colunas da tabela esquerda — na prática, a subconsulta é executada uma vez por linha da tabela esquerda.
▶ Exemplo: Os 3 Pedidos Mais Recentes de Cada Usuário
SELECT
u.name,
recent.order_id,
recent.amount,
recent.created_at
FROM users u
LEFT JOIN LATERAL (
SELECT o.order_id, o.amount, o.created_at
FROM orders o
WHERE o.user_id = u.user_id
ORDER BY o.created_at DESC
LIMIT 3
) recent ON true
ORDER BY u.name, recent.created_at DESC;
Output:
CREATE TABLE
| Abordagem | Pode referenciar tabela esquerda | Suporte Top-N | Desempenho |
|---|---|---|---|
| Subconsulta simples | Não | Precisa de ROW_NUMBER window | Uma varredura |
| LATERAL | Sim | LIMIT direto | Subconsulta executa por linha |
| Função de janela | — | ROW_NUMBER + filtro | Uma varredura |
▶ Exemplo: A Avaliação Mais Recente de Cada Produto
SELECT
p.product_name,
r.review_text,
r.created_at
FROM products p
LEFT JOIN LATERAL (
SELECT review_text, created_at
FROM reviews r
WHERE r.product_id = p.product_id
ORDER BY created_at DESC
LIMIT 1
) r ON true;
Output:
CREATE TABLE
(3) Fundamentos de Desempenho de JOIN
▶ Exemplo: EXPLAIN para Visualizar o Plano de Junção
EXPLAIN
SELECT u.name, o.order_id
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.amount > 50000;
Hash Join
Hash Cond: (o.user_id = u.user_id)
-> Seq Scan on orders
Filter: (amount > 50000)
-> Hash
-> Seq Scan on users
| Estratégia de JOIN | Quando usar | Características |
|---|---|---|
| Nested Loop | Tabela pequena conduz tabela grande | Bom para equi-condições indexadas |
| Hash Join | Equi-junção, sem índice | Constrói uma tabela hash; preferido em grandes volumes |
| Merge Join | Dados já ordenados | Precisa que ambos os lados estejam ordenados |
▶ Exemplo: Consulta Lenta por Falta de Índice
EXPLAIN ANALYZE
SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.email = o.customer_email;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
Se a coluna email não estiver indexada, pode degradar para uma varredura completa de tabela Nested Loop.
▶ Exemplo: Adicionar um Índice para Acelerar o JOIN
CREATE INDEX idx_orders_user_id ON orders(user_id);
Output:
CREATE TABLE
5. Prática
▶ Exemplo: LEFT JOIN para Contar Pedidos por Usuário (Incluindo 0)
SELECT
u.user_id,
u.name,
COUNT(o.order_id) AS order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.name
ORDER BY order_count DESC;
Output:
count
-------
5
(1 row)
▶ Exemplo: FULL JOIN para Reconciliar Usuários de Dois Sistemas
SELECT
a.user_id AS system_a_id,
a.email AS system_a_email,
b.user_id AS system_b_id,
b.email AS system_b_email
FROM system_a_users a
FULL JOIN system_b_users b ON a.email = b.email
ORDER BY a.user_id NULLS LAST, b.user_id NULLS LAST;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ Exemplo: Auto-Junção para Encontrar Usuários que Fizeram Pedido no Mesmo Dia
SELECT DISTINCT
a.name AS user_a,
b.name AS user_b
FROM orders oa
JOIN users a ON oa.user_id = a.user_id
JOIN orders ob ON DATE(oa.created_at) = DATE(ob.created_at)
JOIN users b ON ob.user_id = b.user_id
WHERE a.user_id < b.user_id;
Output:
CREATE TABLE
6. Exemplo Abrangente
Relatório detalhado de compras de usuários de Charlie — junção de 4 tabelas + LATERAL para pedidos recentes:
SELECT
u.name AS user_name,
u.email,
coalesce(order_summary.total_orders, 0) AS total_orders,
coalesce(order_summary.total_spent, 0) AS total_spent,
recent.order_id AS latest_order_id,
recent.created_at AS latest_order_date,
recent.amount AS latest_amount
FROM users u
LEFT JOIN LATERAL (
SELECT
COUNT(*) AS total_orders,
SUM(amount) AS total_spent
FROM orders o
WHERE o.user_id = u.user_id
) order_summary ON true
LEFT JOIN LATERAL (
SELECT order_id, created_at, amount
FROM orders o
WHERE o.user_id = u.user_id
ORDER BY created_at DESC
LIMIT 1
) recent ON true
ORDER BY total_spent DESC NULLS LAST;
user_name | email | total_orders | total_spent | latest_order_id | latest_order_date | latest_amount
-----------+--------------------+--------------+-------------+-----------------+-----------------------+--------------
Bob | bob@example.com | 5 | 320000 | 205 | 2025-11-20 16:00:00 | 85000
Alice | alice@example.com | 3 | 180000 | 102 | 2025-04-02 14:20:00 | 15000
Charlie | charlie@example.com| 0 | 0 | | |
7. Árvore de Decisão para Seleção de JOIN
flowchart TD
A[Precisa unir múltiplas tabelas?] -->|Não| Z[Nenhum JOIN necessário]
A -->|Sim| B{Precisa manter<br/>linhas não correspondentes?}
B -->|Não| C[INNER JOIN]
B -->|Sim, manter esquerda| D[LEFT JOIN]
B -->|Sim, manter direita| E[RIGHT JOIN]
B -->|Sim, manter ambas| F[FULL JOIN]
C --> G{Precisa de Top-N<br/>ou referenciar colunas da esquerda?}
G -->|Sim| H[LATERAL JOIN]
G -->|Não| I[INNER JOIN simples]
D --> G
style H fill:#c8e6c9
style F fill:#fff9c4
❓ Perguntas Frequentes
P: Filtrar uma coluna da tabela direita com WHERE após um LEFT JOIN o transforma em INNER JOIN? R: Sim. WHERE o.col = 'x' filtra as linhas NULL da tabela direita, o que é equivalente a um INNER JOIN. Em vez disso, mova a condição para a cláusula ON.
P: A ordem dos JOINs de múltiplas tabelas afeta o resultado? R: A ordem dos INNER JOINs não afeta o resultado (logicamente equivalente), mas a ordem dos LEFT JOINs afeta — o lado esquerdo é o lado mantido e não pode ser trocado livremente.
P: Qual a diferença entre USING e ON? R: USING(col) requer colunas com o mesmo nome e uma equi-junção, mesclando essa coluna em uma única coluna de saída; ON é mais flexível, suportando nomes de coluna diferentes e condições complexas.
P: Como o LATERAL difere de uma subconsulta? R: Uma subconsulta simples não pode referenciar colunas da tabela esquerda no mesmo nível FROM; LATERAL pode — é efetivamente uma subconsulta que executa uma vez por linha da tabela esquerda.
P: Para que serve o CROSS JOIN na prática? R: Para gerar permutações (ex.: data × dimensão), gerar sequências, matrizes de relatório e assim por diante. Note que a contagem de linhas resultante é o produto dos tamanhos das duas tabelas.
P: Por que o NATURAL JOIN é desencorajado? R: Ele faz junção implícita em todas as colunas com o mesmo nome; adicionar uma coluna com o mesmo nome altera a lógica de junção e causa bugs difíceis de rastrear. Escreva ON ou USING explicitamente por segurança.
📖 Resumo
- INNER JOIN mantém apenas linhas correspondentes; LEFT JOIN mantém todas da tabela esquerda
- RIGHT JOIN pode ser reescrito como LEFT JOIN com lados trocados; FULL JOIN mantém ambos os lados
- CROSS JOIN produz um produto cartesiano; a junção implícita do NATURAL JOIN requer cautela
- Auto-junção é usada para hierarquias e comparações na mesma tabela; dê a cada tabela um alias distinto
- USING simplifica equi-junções em colunas com mesmo nome; ON suporta condições arbitrárias
- LATERAL JOIN pode referenciar colunas da tabela esquerda, ideal para cenários Top-N
- Una múltiplas tabelas nível por nível ao longo dos relacionamentos; preste atenção à ordem do LEFT JOIN
- EXPLAIN revela a estratégia de JOIN; índices são a chave para o desempenho
📝 Exercícios
- ⭐ Escreva uma consulta INNER JOIN unindo orders e order_items, exibindo a quantidade total (SUM quantity) por pedido.
- ⭐ Use um LEFT JOIN para encontrar produtos que nunca foram comprados (products LEFT JOIN order_items, filtre por NULL).
- ⭐⭐ Una users → orders → order_items → products e exiba o detalhe de compra de cada usuário, incluindo nome do produto e valor da linha.
- ⭐⭐ Use um LATERAL JOIN para encontrar os 2 produtos de maior preço em cada categoria de produto.
- ⭐⭐⭐ Escreva uma instrução SQL usando FULL JOIN para comparar as tabelas de usuários de dois sistemas (system_a_users / system_b_users), marcando registros que existem apenas em A, apenas em B ou em ambos, e conte quantos caem em cada categoria.