Artigo Build·Desenvolvimento·14 min de leitura

SQL JOINs: Guia Completo com INNER, LEFT, RIGHT e FULL

JOINs são a base do SQL relacional. Este guia cobre todos os tipos com diagramas, exemplos prontos para PostgreSQL e MySQL, padrões de anti-join e semi-join, JOIN com várias tabelas, ON vs USING e armadilhas comuns de performance.

Vitor Morais

Por Vitor Morais

Fundador do MochaLabz ·

🗄️

Formate suas queries SQL online

Deixe seus JOINs legíveis com indentação automática. 100% no navegador.

Usar formatador SQL →

JOIN é a operação que combina linhas de duas ou mais tabelas baseada em uma condição (normalmente, uma chave em comum). É o que torna o modelo relacional poderoso: dados normalizados em tabelas separadas voltam a se relacionar sob demanda, sem duplicar nem desnormalizar. Saber escolher o JOIN certo é uma das competências mais marcantes que separam um dev júnior de um sênior em SQL.

Tabelas de exemplo

Os exemplos deste guia usam duas tabelas simples, do tipo que você encontra em qualquer aplicação real:

-- usuarios id | nome ---+----------- 1 | Ana 2 | Bob 3 | Carlos -- pedidos id | usuario_id | produto ---+------------+----------- 1 | 1 | Notebook 2 | 1 | Mouse 3 | 4 | Teclado ← usuário 4 NÃO existe

Repare nas duas situações importantes para entender JOINs: Bob e Carlos existem mas não têm pedido, e o pedido 3 referencia um usuário que não existe (referência órfã, comum em sistemas legados sem integridade referencial).

Visão geral dos tipos de JOIN

Comportamento de cada tipo de JOIN
CritérioMantém da esquerdaMantém da direita
INNER JOINSó com correspondênciaSó com correspondência
LEFT (OUTER) JOINTodosSó com correspondência
RIGHT (OUTER) JOINSó com correspondênciaTodos
FULL (OUTER) JOINTodosTodos
CROSS JOINTodos × todos (produto cartesiano)
SELF JOINTabela junta com ela mesma

INNER JOIN: apenas correspondências

Retorna apenas linhas que têm correspondência em ambas as tabelas. É o JOIN mais comum.

SELECT u.nome, p.produto FROM usuarios u INNER JOIN pedidos p ON u.id = p.usuario_id; -- Resultado: nome | produto -----+---------- Ana | Notebook Ana | Mouse -- Bob e Carlos: sem pedido → fora -- Pedido do usuário 4 (inexistente) → fora

A palavra INNER é opcional

Se você escrever só JOIN, o SQL assume INNER JOIN. Mas seja explícito: melhora legibilidade e evita confusão em code review.

LEFT JOIN (LEFT OUTER JOIN): todos da esquerda

Retorna todas as linhas da tabela à esquerda, mais as que casarem da direita. Onde não há correspondência, as colunas da direita vêm NULL.

SELECT u.nome, p.produto FROM usuarios u LEFT JOIN pedidos p ON u.id = p.usuario_id; -- Resultado: nome | produto -------+---------- Ana | Notebook Ana | Mouse Bob | NULL ← sem pedido Carlos | NULL ← sem pedido

RIGHT JOIN (RIGHT OUTER JOIN): todos da direita

Espelho do LEFT JOIN. Mantém todas as linhas da direita; NULLs aparecem nas colunas da esquerda quando não há correspondência:

SELECT u.nome, p.produto FROM usuarios u RIGHT JOIN pedidos p ON u.id = p.usuario_id; -- Resultado: nome | produto -----+---------- Ana | Notebook Ana | Mouse NULL | Teclado ← pedido órfão (usuário 4 não existe)

Como A LEFT JOIN B é o mesmo que B RIGHT JOIN A com colunas reordenadas, a maioria dos devs prefere usar só LEFT JOIN para padronizar.

FULL OUTER JOIN: tudo de ambos os lados

Retorna todas as linhas das duas tabelas. Onde não há correspondência de um lado, vem NULL no outro:

SELECT u.nome, p.produto FROM usuarios u FULL OUTER JOIN pedidos p ON u.id = p.usuario_id; -- Resultado: nome | produto -------+---------- Ana | Notebook Ana | Mouse Bob | NULL ← usuário sem pedido Carlos | NULL ← usuário sem pedido NULL | Teclado ← pedido órfão

MySQL não tem FULL OUTER JOIN

MySQL não suporta FULL OUTER JOIN nativamente. Para simular, faça UNION entre LEFT JOIN e RIGHT JOIN. Em PostgreSQL, SQL Server e Oracle funciona normalmente.

CROSS JOIN: produto cartesiano

Combina cada linha de A com cada linha de B — sem condição. Úteis em casos pontuais, perigoso em qualquer outro:

-- 3 usuários × 3 pedidos = 9 combinações SELECT u.nome, p.produto FROM usuarios u CROSS JOIN pedidos p; -- Caso de uso real: gerar matriz de meses × categorias -- para relatório de vendas com zeros explícitos. SELECT m.mes, c.nome AS categoria, COALESCE(SUM(v.valor), 0) AS total FROM meses m CROSS JOIN categorias c LEFT JOIN vendas v ON v.mes = m.mes AND v.categoria_id = c.id GROUP BY m.mes, c.nome;

SELF JOIN: tabela junta com ela mesma

Útil para hierarquias (funcionário/gerente, categoria/subcategoria, comentário/resposta). Exige aliases para distinguir as duas “cópias” da mesma tabela:

-- Tabela funcionarios: -- id | nome | gerente_id -- 1 | Ana | NULL ← CEO -- 2 | Bob | 1 -- 3 | Cris | 1 -- Listar funcionários e seus gerentes: SELECT f.nome AS funcionario, g.nome AS gerente FROM funcionarios f LEFT JOIN funcionarios g ON g.id = f.gerente_id; -- Resultado: funcionario | gerente ------------+-------- Ana | NULL Bob | Ana Cris | Ana

ON vs USING vs NATURAL JOIN

Três sintaxes para definir a condição. ON é a mais flexível e recomendada:

-- ON: explícito, qualquer condição SELECT * FROM usuarios u JOIN pedidos p ON u.id = p.usuario_id; -- USING: quando a coluna tem o mesmo nome nos dois lados SELECT * FROM usuarios u JOIN pedidos p USING (usuario_id); -- NATURAL JOIN: junta por TODAS as colunas com o mesmo nome -- ⚠️ EVITE — frágil, quebra silenciosamente quando schema muda SELECT * FROM usuarios NATURAL JOIN pedidos;

Evite NATURAL JOIN em produção

NATURAL JOIN parece elegante mas é armadilha: se alguém adicionar uma coluna created_at em ambas as tabelas, o JOIN passa a casar por ela também — silenciosamente quebrando consultas que funcionavam.

JOIN com 3 ou mais tabelas

Encadeie JOINs em sequência. Cada um é avaliado contra o resultado dos anteriores:

SELECT u.nome, p.produto, e.cidade, c.titulo FROM usuarios u JOIN pedidos p ON p.usuario_id = u.id JOIN enderecos e ON e.usuario_id = u.id LEFT JOIN cupons c ON c.pedido_id = p.id; -- Pratique: identifique mentalmente qual é a "tabela-base" -- (geralmente a primeira do FROM) e siga JOIN por JOIN.

Padrões avançados: anti-join e semi-join

Anti-join: registros SEM correspondência

“Clientes sem nenhum pedido”, “produtos sem venda” — anti-join é o padrão. Duas formas comuns:

-- LEFT JOIN + IS NULL SELECT u.nome FROM usuarios u LEFT JOIN pedidos p ON p.usuario_id = u.id WHERE p.id IS NULL; -- NOT EXISTS (geralmente mais eficiente em Postgres) SELECT u.nome FROM usuarios u WHERE NOT EXISTS ( SELECT 1 FROM pedidos p WHERE p.usuario_id = u.id );

Semi-join: existe pelo menos uma correspondência

“Clientes que fizeram pelo menos um pedido” — sem duplicar quando o cliente tem vários:

-- EXISTS (recomendado, sem duplicatas) SELECT u.nome FROM usuarios u WHERE EXISTS ( SELECT 1 FROM pedidos p WHERE p.usuario_id = u.id ); -- INNER JOIN + DISTINCT (alternativa, exige DISTINCT) SELECT DISTINCT u.nome FROM usuarios u JOIN pedidos p ON p.usuario_id = u.id;

ON vs WHERE: a diferença que pega gente em OUTER JOIN

Em INNER JOIN, condições em ON ou WHERE têm o mesmo efeito. Em OUTER JOIN são fundamentalmente diferentes:

-- Condição em ON: aplicada ANTES do OUTER, mantém NULLs SELECT u.nome, p.produto FROM usuarios u LEFT JOIN pedidos p ON p.usuario_id = u.id AND p.produto = 'Notebook'; -- Resultado: TODOS os usuários, com produto preenchido -- só onde houver pedido de Notebook (NULL no resto) -- Condição em WHERE: aplicada DEPOIS, transforma em INNER SELECT u.nome, p.produto FROM usuarios u LEFT JOIN pedidos p ON p.usuario_id = u.id WHERE p.produto = 'Notebook'; -- Resultado: SÓ usuários com Notebook (NULLs filtrados)

Regra mental

Em LEFT JOIN, condições sobre a tabela da direita devem ir no ON se você quer manter o efeito OUTER. WHERE filtra DEPOIS e efetivamente vira INNER JOIN.

Performance: armadilhas comuns

  • Falta de índice nas colunas de JOIN: sem índice, o planner faz scan completo. Sempre indexe colunas de chave estrangeira. Veja o guia de performance de SQL com índices.
  • Tipos diferentes nas colunas de JOIN: comparar INTEGER com VARCHAR força conversão linha por linha — ignora índice.
  • SELECT *: traz todas as colunas das tabelas joinadas, inflando IO sem necessidade.
  • JOINs em cadeia sem necessidade: 8 JOINs para montar um relatório que poderia ser feito em 2 com agregação prévia.
  • Cartesianos acidentais: condição ON errada ou esquecida vira CROSS JOIN escondido.
  • Funções nas colunas de JOIN: ON LOWER(a.email) = LOWER(b.email) impede uso de índice direto.

EXPLAIN: a melhor amiga do JOIN lento

Antes de otimizar no escuro, use EXPLAIN (ou EXPLAIN ANALYZE no Postgres) para ver a estratégia escolhida pelo planner:

-- PostgreSQL EXPLAIN ANALYZE SELECT u.nome, COUNT(p.id) FROM usuarios u LEFT JOIN pedidos p ON p.usuario_id = u.id GROUP BY u.nome; -- O que olhar no plano: -- - "Seq Scan" em tabela grande sem WHERE → falta de índice -- - "Hash Join" vs "Nested Loop" vs "Merge Join" → estratégia -- - "rows" estimado vs "actual rows" → estatísticas estão certas? -- - "filter" lendo muitas linhas → índice ineficiente

Diferenças entre PostgreSQL e MySQL

Particularidades de JOINs por SGBD
CritérioPostgreSQLMySQL
FULL OUTER JOIN nativo❌ — simular com UNION
LATERAL JOIN✅ (8.0+)
USING/NATURAL
Otimização de subquery → JOINForteBoa (8.0+)
Hash Join✅ (8.0+)
Index HintsLimitadoForte (USE/FORCE INDEX)

Casos de uso por tipo de JOIN

Qual JOIN usar em cada cenário
CritérioJOIN recomendado
Relatório com dados completos dos dois ladosINNER JOIN
Listar todos os usuários e seus pedidos (se houver)LEFT JOIN
Encontrar usuários SEM pedidosLEFT JOIN + WHERE NULL ou NOT EXISTS
Comparar dois conjuntos para diffFULL OUTER JOIN
Hierarquia (funcionário/gerente)SELF JOIN
Matriz pré-agregada (meses × categorias)CROSS JOIN + LEFT JOIN agregado
Existência simples (tem pedido?)EXISTS

Checklist de JOINs bem escritos

  • ✅ JOIN explícito (use INNER JOIN, não vírgula).
  • ✅ Aliases curtos e consistentes nas tabelas.
  • ✅ Condição na cláusula ON, não no WHERE, para OUTER JOIN.
  • ✅ Colunas de JOIN com índice e tipo igual.
  • SELECT só com colunas necessárias.
  • ✅ Anti-join via NOT EXISTS quando viável.
  • ✅ EXPLAIN rodado em queries críticas antes de produção.
  • ✅ Sem NATURAL JOIN.
  • ✅ Sem cartesianos acidentais (revisar condição ON).

Perguntas frequentes

O que é um JOIN em SQL?+

JOIN é a operação que combina linhas de duas ou mais tabelas baseada em uma condição (geralmente uma chave em comum). É o que torna o modelo relacional poderoso: dados normalizados em tabelas separadas voltam a ser relacionados via JOINs sob demanda. Os principais tipos são INNER, LEFT/RIGHT OUTER, FULL OUTER, CROSS e SELF JOIN.

Qual a diferença entre INNER JOIN e LEFT JOIN?+

INNER JOIN retorna só as linhas que têm correspondência em ambas as tabelas. LEFT JOIN retorna todas as linhas da tabela à esquerda, com NULL nas colunas da direita quando não há correspondência. Use INNER quando precisa de dados completos dos dois lados; LEFT quando precisa de tudo da esquerda mais o que houver da direita.

RIGHT JOIN é igual a LEFT JOIN invertido?+

Sim. A LEFT JOIN B é equivalente a B RIGHT JOIN A. Por isso RIGHT JOIN é raro na prática — a maioria dos devs prefere reescrever como LEFT JOIN, que tem ordem mental mais natural ("todos da primeira tabela mais o que houver da segunda"). PostgreSQL e MySQL aceitam ambos.

Como encontrar registros que NÃO têm correspondência (anti-join)?+

Padrão clássico: LEFT JOIN com WHERE coluna_da_direita IS NULL. Exemplo: "clientes sem nenhum pedido" = SELECT c.* FROM clientes c LEFT JOIN pedidos p ON p.cliente_id = c.id WHERE p.id IS NULL. Em PostgreSQL, NOT EXISTS costuma ter performance equivalente ou melhor.

Posso fazer JOIN entre 3 ou mais tabelas?+

Sim, encadeando JOINs. Exemplo: SELECT u.nome, p.produto, e.cidade FROM usuarios u JOIN pedidos p ON p.usuario_id = u.id JOIN enderecos e ON e.usuario_id = u.id. Cada JOIN é avaliado em sequência (logicamente). Para queries com muitos JOINs, ative o EXPLAIN para ver a ordem real escolhida pelo planner.

JOIN é mais rápido ou subquery?+

Em geral, planners modernos (PostgreSQL, MySQL 8+, SQL Server) reescrevem subqueries em JOINs internamente quando isso é vantajoso, então a diferença de performance costuma ser mínima. JOIN tende a ser mais legível para combinar tabelas. EXISTS/IN com subquery é melhor quando você só precisa testar existência. Use EXPLAIN para confirmar.

Qual a diferença entre ON e WHERE em JOINs?+

ON define a condição de junção (que linhas se associam). WHERE filtra o resultado depois. Em INNER JOIN são equivalentes para a maioria dos casos. Em OUTER JOIN são MUITO diferentes: condição em ON é aplicada antes da junção (mantém NULL); WHERE depois transforma OUTER em INNER se filtrar a coluna do lado opcional.

CROSS JOIN é útil ou só perigoso?+

CROSS JOIN gera o produto cartesiano (toda linha de A combinada com toda linha de B). Sem WHERE, é desastre — 1000 × 1000 = 1 milhão de linhas. Mas é útil em casos específicos: gerar combinações para tabelas de calendário, popular matrizes de teste, criar buckets de relatório. Use com consciência.

#sql#join#inner join#left join#outer join#banco de dados#postgresql#mysql#performance#explain

Continue lendo