PostgreSQL: Consultas de Junção de Múltiplas Tabelas no…

Última atualização: 2026-08-26

1. O Que Você Vai Aprender


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:

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
100%
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

SQL
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;
TEXT 📖 Somente leitura
 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

SQL
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;
TEXT 📖 Somente leitura
 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

SQL
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:

TEXT 📖 Somente leitura
 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

SQL
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;
TEXT 📖 Somente leitura
 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

SQL
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:

TEXT 📖 Somente leitura
 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

SQL
SELECT
  d.department_name,
  p.project_name
FROM departments d
CROSS JOIN projects p;

Output:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

3 departamentos × 5 projetos = 15 linhas.

▶ Exemplo: NATURAL JOIN (use com cautela)

SQL
SELECT * FROM users
NATURAL JOIN orders;

Output:

TEXT 📖 Somente leitura
 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

SQL
SELECT
  u.name,
  o.order_id,
  o.amount
FROM users u
JOIN orders o USING (user_id);

Output:

TEXT 📖 Somente leitura
 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

SQL
SELECT
  e.name AS employee,
  m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;
TEXT 📖 Somente leitura
 employee | manager
----------+---------
 Alice    | David
 Bob      | Alice
 Charlie  | Alice
 David    |

▶ Exemplo: Comparar Preços de Produtos na Mesma Categoria

SQL
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:

TEXT 📖 Somente leitura
 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

SQL
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;
TEXT 📖 Somente leitura
 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

SQL
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:

TEXT 📖 Somente leitura
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

SQL
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:

TEXT 📖 Somente leitura
CREATE TABLE

(3) Fundamentos de Desempenho de JOIN

▶ Exemplo: EXPLAIN para Visualizar o Plano de Junção

SQL
EXPLAIN
SELECT u.name, o.order_id
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.amount > 50000;
TEXT 📖 Somente leitura
 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

SQL
EXPLAIN ANALYZE
SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.email = o.customer_email;

Output:

TEXT 📖 Somente leitura
 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

SQL
CREATE INDEX idx_orders_user_id ON orders(user_id);

Output:

TEXT 📖 Somente leitura
CREATE TABLE

5. Prática

▶ Exemplo: LEFT JOIN para Contar Pedidos por Usuário (Incluindo 0)

SQL
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:

TEXT 📖 Somente leitura
 count 
-------
     5
(1 row)

▶ Exemplo: FULL JOIN para Reconciliar Usuários de Dois Sistemas

SQL
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:

TEXT 📖 Somente leitura
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ Exemplo: Auto-Junção para Encontrar Usuários que Fizeram Pedido no Mesmo Dia

SQL
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:

TEXT 📖 Somente leitura
CREATE TABLE

6. Exemplo Abrangente

Relatório detalhado de compras de usuários de Charlie — junção de 4 tabelas + LATERAL para pedidos recentes:

SQL
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;
TEXT 📖 Somente leitura
 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

100%
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


📝 Exercícios

  1. ⭐ Escreva uma consulta INNER JOIN unindo orders e order_items, exibindo a quantidade total (SUM quantity) por pedido.
  2. ⭐ Use um LEFT JOIN para encontrar produtos que nunca foram comprados (products LEFT JOIN order_items, filtre por NULL).
  3. ⭐⭐ Una users → orders → order_items → products e exiba o detalhe de compra de cada usuário, incluindo nome do produto e valor da linha.
  4. ⭐⭐ Use um LATERAL JOIN para encontrar os 2 produtos de maior preço em cada categoria de produto.
  5. ⭐⭐⭐ 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.
Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%