Aprenda os 6 JOINs SQL que todo analista deve conhecer — INNER, LEFT, RIGHT, FULL OUTER, CROSS e SELF — com exemplos práticos e armadilhas comuns.

Um SQL JOIN permite combinar linhas de duas (ou mais) tabelas em um único resultado ao casá-las por uma coluna relacionada — normalmente um ID.
A maioria dos bancos de dados é propositalmente dividida em tabelas separadas para evitar repetir informação. Por exemplo, o nome de um cliente fica na tabela customers, enquanto as compras ficam na tabela orders. JOINs são como você reconecta essas partes quando precisa de respostas.
Por isso JOINs aparecem em todo tipo de relatório e análise:
Sem JOINs, você teria que rodar consultas separadas e combinar resultados manualmente — lento, sujeito a erros e difícil de repetir.
Se você está construindo produtos sobre um banco relacional (dashboards, painéis administrativos, ferramentas internas, portais), JOINs também transformam “tabelas brutas” em visões voltadas ao usuário. Plataformas como Koder.ai (que geram apps React + Go + PostgreSQL a partir de chat) ainda dependem de bons fundamentos de JOIN para páginas de lista, relatórios e telas de reconciliação — a lógica do banco não desaparece só porque o desenvolvimento ficou mais rápido.
Este guia foca em seis JOINs que cobrem a maior parte do trabalho diário em SQL:
A sintaxe de JOIN é muito similar na maioria dos bancos (PostgreSQL, MySQL, SQL Server, SQLite). Há algumas diferenças — especialmente no suporte a FULL OUTER JOIN e em comportamentos de borda —, mas os conceitos e padrões centrais se transferem bem.
Para manter os exemplos simples, usaremos três tabelas pequenas que refletem um cenário comum: clientes fazem pedidos, e pedidos podem (ou não) ter pagamentos.
Uma nota antes de começar: as tabelas de amostra abaixo mostram só algumas colunas, mas consultas posteriores referenciam campos adicionais (como order_date, created_at, status ou paid_at) para demonstrar padrões comuns. Considere essas colunas como campos típicos que você teria em esquemas de produção.
Chave primária: customer_id
| customer_id | name |
|---|---|
| 1 | Ava |
| 2 | Ben |
| 3 | Chen |
| 4 | Dia |
Chave primária: order_id
Chave estrangeira: customer_id → customers.customer_id
| order_id | customer_id | order_total |
|---|---|---|
| 101 | 1 | 50 |
| 102 | 1 | 120 |
| 103 | 2 | 35 |
| 104 | 5 | 70 |
Note que order_id = 104 referencia customer_id = 5, que não existe em customers. Essa “correspondência ausente” é útil para ver como LEFT JOIN, RIGHT JOIN e FULL OUTER JOIN se comportam.
Chave primária: payment_id
Chave estrangeira: order_id → orders.order_id
| payment_id | order_id | amount |
|---|---|---|
| 9001 | 101 | 50 |
| 9002 | 102 | 60 |
| 9003 | 102 | 60 |
| 9004 | 999 | 25 |
Dois detalhes importantes para ensinar aqui:
order_id = 102 tem duas linhas de pagamento (pagamento dividido). Ao juntar orders com payments, esse pedido aparecerá duas vezes — é aí que duplicatas surpreendem as pessoas.payment_id = 9004 referencia order_id = 999, que não existe em orders. Isso cria outro caso “sem correspondência”.orders com payments repetirá o pedido 102 porque tem dois pagamentos.Um INNER JOIN retorna apenas as linhas onde há correspondência em ambas as tabelas. Se um cliente não tem pedidos, ele não aparecerá. Se um pedido aponta para um cliente que não existe (dados errados), esse pedido também não aparecerá.
Você escolhe uma tabela “esquerda”, junta uma tabela “direita” e conecta com uma condição em ON.
SELECT
c.customer_id,
c.name,
o.order_id,
o.order_date
FROM customers c
INNER JOIN orders o
ON o.customer_id = c.customer_id;
A linha chave é ON o.customer_id = c.customer_id: ela diz ao SQL como as linhas se relacionam.
Se você quer a lista apenas de clientes que fizeram pelo menos um pedido (e os detalhes do pedido), INNER JOIN é a escolha natural:
SELECT
c.name,
o.order_id,
o.total_amount
FROM customers c
INNER JOIN orders o
ON o.customer_id = c.customer_id
ORDER BY o.order_id;
Isso é útil para coisas como “enviar um email de acompanhamento de pedido” ou “calcular receita por cliente” (quando você só se importa com clientes com compras).
Se você escrever um join e esquecer o ON (ou fizer join nas colunas erradas), pode criar um produto cartesiano (todo cliente combinado com todo pedido) ou gerar correspondências incorretas.
Ruim (não faça isto):
SELECT c.name, o.order_id
FROM customers c
JOIN orders o;
Sempre garanta uma condição clara em ON (ou USING quando aplicável — coberto mais adiante).
Um LEFT JOIN retorna todas as linhas da tabela esquerda, e adiciona dados da tabela direita quando existir. Se não houver correspondência, as colunas da direita aparecem como NULL.
Use LEFT JOIN quando você quer uma lista completa da sua tabela primária, mais dados relacionados opcionais.
Exemplo: “Mostre todos os clientes, e inclua seus pedidos se tiverem.”
SELECT
c.customer_id,
c.name,
o.order_id,
o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
ORDER BY c.customer_id;
o.order_id (e outras colunas de orders) será NULL.Um motivo muito comum para usar LEFT JOIN é achar itens que não têm registros relacionados.
Exemplo: “Quais clientes nunca fizeram um pedido?”
SELECT
c.customer_id,
c.name
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
WHERE o.order_id IS NULL;
Esse WHERE ... IS NULL mantém apenas as linhas da tabela esquerda onde o join não encontrou correspondência.
LEFT JOIN pode “duplicar” linhas da tabela esquerda quando existem múltiplas linhas correspondentes à direita.
Se um cliente tem 3 pedidos, esse cliente aparecerá 3 vezes — uma vez por pedido. Isso é esperado, mas pode surpreender se você estiver tentando contar clientes.
Por exemplo, isso conta pedidos (não clientes):
SELECT COUNT(*)
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id;
Se seu objetivo é contar clientes, normalmente você contará a chave do cliente (por exemplo, COUNT(DISTINCT c.customer_id)), dependendo do que você mede.
Um RIGHT JOIN mantém todas as linhas da tabela direita, e só as linhas correspondentes da esquerda. Se não houver correspondência, as colunas da esquerda aparecem como NULL. É essencialmente a imagem espelhada de um LEFT JOIN.
Com nossas tabelas, imagine que você quer listar todos os pagamentos, mesmo que não seja possível ligá-los a um pedido (talvez o pedido foi deletado ou os dados de pagamento estão bagunçados).
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount,
p.paid_at
FROM orders o
RIGHT JOIN payments p
ON o.order_id = p.order_id;
O que você recebe:
payments está à direita).o.order_id e o.customer_id serão NULL.Na maioria das vezes, você pode reescrever um RIGHT JOIN como LEFT JOIN trocando a ordem das tabelas:
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount,
p.paid_at
FROM payments p
LEFT JOIN orders o
ON o.order_id = p.order_id;
Isso retorna o mesmo resultado, mas muita gente acha mais claro: você começa com a tabela “principal” (aqui, payments) e então puxa dados relacionados opcionalmente.
Muitos guias de estilo SQL desencorajam RIGHT JOIN porque força o leitor a inverter mentalmente o padrão comum:
Quando relacionamentos opcionais são sempre escritos como LEFT JOIN, as consultas ficam mais fáceis de ler.
RIGHT JOIN pode ser útil ao editar uma consulta existente quando você percebe que a tabela que deve ser preservada está atualmente à direita. Em vez de reescrever toda a consulta (especialmente uma longa com vários joins), trocar um join para RIGHT JOIN pode ser uma mudança rápida e de baixo risco.
Um FULL OUTER JOIN retorna todas as linhas de ambas as tabelas.
INNER JOIN).NULL nas colunas da direita.NULL nas colunas da esquerda.Um caso clássico é reconciliar orders vs. payments:
Exemplo:
SELECT
o.order_id,
o.customer_id,
p.payment_id,
p.amount
FROM orders o
FULL OUTER JOIN payments p
ON p.order_id = o.order_id;
FULL OUTER JOIN é suportado em PostgreSQL, SQL Server e Oracle.
Não está disponível em MySQL e SQLite (você precisará de um contorno).
Se seu banco não suporta FULL OUTER JOIN, você pode simulá-lo combinando:
orders (com pagamentos quando disponíveis), epayments que não casaram com um pedido.Um padrão comum:
SELECT o.order_id, o.customer_id, p.payment_id, p.amount
FROM orders o
LEFT JOIN payments p
ON p.order_id = o.order_id
UNION
SELECT o.order_id, o.customer_id, p.payment_id, p.amount
FROM orders o
RIGHT JOIN payments p
ON p.order_id = o.order_id;
Dica: quando você vê NULLs de um lado, esse é o sinal de que a linha estava “faltando” na outra tabela — exatamente o que você quer para auditorias e reconciliações.
Um CROSS JOIN retorna todas as combinações possíveis de linhas entre duas tabelas. Se a tabela A tem 3 linhas e a tabela B tem 4 linhas, o resultado terá 3 × 4 = 12 linhas. Isso também é chamado de produto cartesiano.
Isso pode assustar — e pode —, mas é genuinamente útil quando você quer gerar combinações.
Imagine que você mantém opções de produto em tabelas separadas:
sizes: S, M, Lcolors: Red, BlueUm CROSS JOIN pode gerar todas as variantes (útil para criar SKUs, pré-construir um catálogo ou testar):
SELECT
s.size,
c.color
FROM sizes AS s
CROSS JOIN colors AS c;
Resultado (3 × 2 = 6 linhas):
Porque a contagem de linhas multiplica, CROSS JOIN pode explodir rapidamente:
Isso pode deixar consultas lentas, sobrecarregar memória e produzir uma saída inútil. Se precisar de combinações, mantenha as tabelas de entrada pequenas e considere limites ou filtros controlados.
Um SELF JOIN é exatamente o que parece: você junta uma tabela consigo mesma. Isso é útil quando uma linha numa tabela se relaciona com outra linha da mesma tabela — mais comumente em relações pai/filho como funcionários e seus gerentes.
Como você está usando a mesma tabela duas vezes, precisa dar a cada “cópia” um alias diferente. Aliases deixam a consulta legível e dizem ao SQL qual lado você quer referenciar.
Um padrão comum é:
e para o empregadom para o gerenteImagine uma tabela employees com:
idnamemanager_id (aponta para o id de outro empregado)Para listar cada empregado com o nome do gerente:
SELECT
e.id,
e.name AS employee_name,
m.name AS manager_name
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id;
Note que a consulta usa LEFT JOIN, não INNER JOIN. Isso importa porque alguns empregados podem não ter gerente (por exemplo, o CEO). Nesses casos, manager_id costuma ser NULL, e um LEFT JOIN mantém a linha do empregado mostrando manager_name como NULL.
Se você usasse INNER JOIN, esses empregados de topo desapareceriam do resultado porque não há uma linha de gerente correspondente.
Um JOIN não “sabe” automaticamente como duas tabelas se relacionam — você precisa dizer. Essa relação é definida na condição do join, e ela fica ao lado do JOIN porque explica como as tabelas batem, não como você quer filtrar o resultado final.
ON: o mais flexível (e o mais comum)Use ON quando quiser controle total sobre a lógica de correspondência — colunas com nomes diferentes, condições múltiplas ou regras extras.
SELECT
c.customer_id,
c.name,
o.order_id,
o.created_at
FROM customers AS c
INNER JOIN orders AS o
ON o.customer_id = c.customer_id;
ON também é onde você define correspondências mais complexas (por exemplo, combinando em duas colunas) sem transformar sua consulta em um jogo de adivinhação.
USING: mais curto, mas só para colunas com mesmo nomeAlguns bancos (como PostgreSQL e MySQL) suportam USING. É um atalho conveniente quando ambas as tabelas têm uma coluna com o mesmo nome e você quer juntar por ela.
SELECT
customer_id,
name,
order_id
FROM customers
JOIN orders
USING (customer_id);
Um benefício: USING normalmente retorna apenas uma coluna customer_id no resultado (em vez de duas cópias).
Após juntar tabelas, nomes de coluna costumam se sobrepor (id, created_at, status). Se você escrever SELECT id, o banco pode lançar um erro de “coluna ambígua” — ou pior, você pode ler a id errada.
Prefira prefixos de tabela (ou aliases) para clareza:
SELECT c.customer_id, o.order_id
FROM customers AS c
JOIN orders AS o
ON o.customer_id = c.customer_id;
SELECT * em queries com joinsSELECT * fica confuso rapidamente com joins: você puxa colunas desnecessárias, corre risco de nomes duplicados e dificulta entender o que a consulta deveria produzir.
Em vez disso, selecione só as colunas que precisa. O resultado fica mais limpo, fácil de manter e muitas vezes mais eficiente — especialmente quando as tabelas são largas.
Quando você junta tabelas, WHERE e ON ambos “filtram”, mas em momentos diferentes.
Essa diferença de momento é a razão pela qual pessoas acidentalmente transformam um LEFT JOIN em um INNER JOIN.
Suponha que você quer todos os clientes, mesmo aqueles sem pedidos pagos recentes.
SELECT c.customer_id, c.name, o.order_id, o.status, o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
WHERE o.status = 'PAID'
AND o.order_date \u003e= DATE '2025-01-01';
Problema: para clientes sem pedido correspondente, o.status e o.order_date são NULL. A cláusula WHERE rejeita essas linhas, então os clientes não correspondentes desaparecem — seu LEFT JOIN se comporta como INNER JOIN.
SELECT c.customer_id, c.name, o.order_id, o.status, o.order_date
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.customer_id
AND o.status = 'PAID'
AND o.order_date \u003e= DATE '2025-01-01';
Agora clientes sem pedidos qualificados ainda aparecem (com colunas de pedido como NULL), que normalmente é o objetivo de um LEFT JOIN.
WHERE o.order_id IS NOT NULL explicitamente).Joins não só “adicionam colunas” — eles também podem multiplicar linhas. Isso geralmente está correto, mas frequentemente surpreende quando totais dobram (ou pior).
Um join retorna uma linha de saída para cada par de linhas que correspondem.
customers com orders, cada cliente pode aparecer múltiplas vezes — uma por pedido.orders com payments e cada pedido pode ter múltiplos pagamentos, você pode obter múltiplas linhas por pedido. Se você também juntar com outra tabela “muitos” (como order_items), você pode criar um efeito de multiplicação: payments × items por pedido.Se seu objetivo é “uma linha por cliente” ou “uma linha por pedido”, resuma o lado “muitos” primeiro, depois junte.
-- Uma linha por pedido a partir de payments
WITH payment_totals AS (
SELECT
order_id,
SUM(amount) AS total_paid,
COUNT(*) AS payment_count
FROM payments
GROUP BY order_id
)
SELECT
o.order_id,
o.customer_id,
COALESCE(pt.total_paid, 0) AS total_paid,
COALESCE(pt.payment_count, 0) AS payment_count
FROM orders o
LEFT JOIN payment_totals pt
ON pt.order_id = o.order_id;
Isso mantém a forma do join previsível: uma linha por pedido continua sendo uma linha por pedido.
SELECT DISTINCT pode fazer as duplicatas parecerem resolvidas, mas pode esconder o problema real:
Use apenas quando tiver certeza de que as duplicatas são meramente acidentais e você entende por que ocorreram.
Antes de confiar nos resultados, compare contagens de linhas:
orders).JOINs costumam ser culpados por “consultas lentas”, mas a causa real geralmente é a quantidade de dados que você pede para combinar e o quão fácil é encontrar as linhas que batem.
Pense em um índice como o índice de um livro. Sem ele, o banco pode precisar ler muitas linhas para achar correspondências. Com um índice na chave de join (por exemplo, customers.customer_id e orders.customer_id), o banco pode pular direto para as linhas relevantes mais rapidamente.
Você não precisa conhecer os detalhes internos para usar isso bem: se uma coluna é frequentemente usada para casar linhas (ON a.id = b.a_id), é um bom candidato para ter um índice.
Sempre que possível, faça join em identificadores estáveis e únicos:
customers.customer_id = orders.customer_idcustomers.email = orders.email ou customers.name = orders.nameNomes mudam e podem se repetir. Emails podem mudar, estar faltando ou ter diferenças de case/formato. IDs são projetados para correspondência consistente e normalmente são indexados.
Dois hábitos tornam JOINs sensivelmente mais rápidos:
SELECT * ao juntar várias tabelas — colunas extras aumentam uso de memória e rede.Exemplo: limite pedidos primeiro, depois junte:
SELECT c.customer_id, c.name, o.order_id, o.created_at
FROM customers c
JOIN (
SELECT order_id, customer_id, created_at
FROM orders
WHERE created_at \u003e= DATE '2025-01-01'
) o
ON o.customer_id = c.customer_id;
Se você estiver iterando nessas consultas dentro de uma construção de app (por exemplo, criando uma página de relatório com PostgreSQL), ferramentas como Koder.ai podem acelerar o scaffolding — esquema, endpoints, UI — enquanto você mantém o controle da lógica de JOIN que determina a correção.
NULL quando ausente)NULL quando ausente)NULLUm JOIN SQL combina linhas de duas (ou mais) tabelas em um único conjunto de resultados ao relacionar colunas relacionadas — na maioria das vezes uma chave primária com uma chave estrangeira (por exemplo, customers.customer_id = orders.customer_id). É assim que você “reconecta” tabelas normalizadas quando precisa gerar relatórios, auditorias ou análises.
Use INNER JOIN quando você quer apenas as linhas em que a relação existe em ambas as tabelas.
É ideal para “relações confirmadas”, como listar somente clientes que realmente fizeram pedidos.
Use LEFT JOIN quando precisar de todas as linhas da sua tabela principal (esquerda), mais dados relacionados opcionais da tabela à direita.
Para encontrar “não correspondências”, faça o join e depois filtre o lado direito para NULL:
c.customer_id, c.name
customers c
orders o o.customer_id c.customer_id
o.order_id ;
RIGHT JOIN mantém todas as linhas da tabela à direita e preenche com NULL as colunas da esquerda quando não há correspondência. Muitas equipes evitam porque a leitura fica “ao contrário”.
Na maioria dos casos, você pode reescrever como LEFT JOIN invertendo a ordem das tabelas:
FROM payments p
orders o o.order_id p.order_id
Use FULL OUTER JOIN para reconciliação: você quer correspondências, linhas só à esquerda e linhas só à direita em uma única saída.
É ótimo para auditorias como “pedidos sem pagamento” e “pagamentos sem pedido”, pois os lados não correspondentes aparecem com NULL.
Alguns bancos (notavelmente MySQL e SQLite) não suportam FULL OUTER JOIN diretamente. Uma solução comum é combinar duas consultas:
orders LEFT JOIN paymentspaymentsGeralmente isso é feito com UNION (ou com cuidado) para manter os registros “somente à esquerda” e “somente à direita”.
Um CROSS JOIN retorna todas as combinações possíveis de linhas entre duas tabelas (produto cartesiano). É útil para gerar cenários (como tamanhos × cores) ou construir uma grade de calendário.
Cuidado: a contagem de linhas cresce rapidamente, podendo explodir o tamanho da saída e deixar as consultas lentas se as entradas não forem pequenas e controladas.
Um self join é juntar uma tabela com ela mesma para relacionar linhas dentro da mesma tabela (comum em hierarquias como empregado → gerente).
Você deve usar aliases para distinguir as duas “cópias”:
FROM employees e
LEFT JOIN employees m
ON e.manager_id = m.id
ON define como as linhas se correspondem durante o join; WHERE filtra depois que o resultado do join já foi formado. Com LEFT JOIN, uma condição WHERE no lado direito pode remover linhas NULL e transformar o comportamento em um INNER JOIN.
Se quiser manter todas as linhas da esquerda, mas restringir quais linhas da direita podem corresponder, coloque o filtro do lado direito em .
Joins podem multiplicar linhas quando a relação é um-para-muitos (ou muitos-para-muitos). Por exemplo, um pedido com dois pagamentos aparece duas vezes ao juntar orders com payments.
Para manter “uma linha por pedido/cliente”, agregue primeiro o lado “muitos” (por exemplo, SUM(amount) agrupado por order_id) e então faça o join. Use DISTINCT apenas como último recurso, pois pode mascarar problemas reais e estragar totais.
UNION ALLON