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.
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 existeRepare 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
| Critério | Mantém da esquerda | Mantém da direita |
|---|---|---|
| INNER JOIN | Só com correspondência | Só com correspondência |
| LEFT (OUTER) JOIN | Todos | Só com correspondência |
| RIGHT (OUTER) JOIN | Só com correspondência | Todos |
| FULL (OUTER) JOIN | Todos | Todos |
| CROSS JOIN | Todos × todos (produto cartesiano) | |
| SELF JOIN | Tabela 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) → foraA 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 pedidoRIGHT 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ãoMySQL 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 | AnaON 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 ineficienteDiferenças entre PostgreSQL e MySQL
| Critério | PostgreSQL | MySQL |
|---|---|---|
| FULL OUTER JOIN nativo | ✅ | ❌ — simular com UNION |
| LATERAL JOIN | ✅ | ✅ (8.0+) |
| USING/NATURAL | ✅ | ✅ |
| Otimização de subquery → JOIN | Forte | Boa (8.0+) |
| Hash Join | ✅ | ✅ (8.0+) |
| Index Hints | Limitado | Forte (USE/FORCE INDEX) |
Casos de uso por tipo de JOIN
| Critério | JOIN recomendado |
|---|---|
| Relatório com dados completos dos dois lados | INNER JOIN |
| Listar todos os usuários e seus pedidos (se houver) | LEFT JOIN |
| Encontrar usuários SEM pedidos | LEFT JOIN + WHERE NULL ou NOT EXISTS |
| Comparar dois conjuntos para diff | FULL 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 noWHERE, para OUTER JOIN. - ✅ Colunas de JOIN com índice e tipo igual.
- ✅
SELECTsó 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.
Continue lendo
SQL para Iniciantes (2026): Guia Prático com Formatação, Exemplos e Boas Práticas
Guia de SQL do zero: SELECT, WHERE, JOIN, GROUP BY, tipos de dados, formatação consistente e erros que iniciantes cometem em produção.
UUID como Chave Primária: Vantagens, Problemas e Alternativas (2026)
Tudo sobre usar UUID como chave primária em PostgreSQL e MySQL: por que vale, problema de fragmentação do v4, como UUID v7 resolve, comparativo com BIGINT, ULID, NanoID, CUID2 e Snowflake.
DATETIME vs TIMESTAMP no Banco de Dados (Guia 2026): MySQL e PostgreSQL
Comparativo técnico completo: armazenamento, fuso horário, range, tamanho, comportamento em mudanças de TZ, TIMESTAMPTZ, problema 2038 e migração.
Performance SQL com Índices: Guia Completo 2026 (PostgreSQL e MySQL)
Aprenda a usar índices em SQL para acelerar queries de segundos para milissegundos. Tipos de índice (B-tree, GIN, GiST, BRIN), índices compostos, parciais, funcionais, EXPLAIN ANALYZE, anti-padrões e checklist de manutenção.