JOINs: INNER, LEFT, RIGHT, FULL e CROSS JOIN

JOIN combina relações segundo uma condição de correspondência
Na aula anterior, construímos consultas previsíveis com SELECT, filtros, ordenação e valores NULL. Agora podemos consultar fatos que foram corretamente separados em tabelas diferentes.
Um JOIN recebe duas relações, compara pares de linhas segundo uma condição e produz uma nova relação. O tipo da junção determina o que acontece com as linhas que encontram correspondência — e com as que não encontram.
SELECT
t.titulo AS trilha,
m.titulo AS modulo
FROM trilha AS t
INNER JOIN modulo AS m
ON m.trilha_id = t.trilha_id
ORDER BY t.titulo, m.ordem;
O resultado não “cola tabelas” de maneira permanente. Ele é uma relação derivada para aquela consulta. As fontes podem ser tabelas, views, subconsultas ou até resultados de outros JOINs.
Antes do tipo, defina a granularidade
Considere duas tabelas simplificadas da plataforma:
trilha
| trilha_id | titulo |
|---|---|
| 10 | Banco de Dados |
| 20 | Redes |
| 30 | Segurança |
matricula
| matricula_id | trilha_id | estudante | situacao |
|---|---|---|---|
| 501 | 10 | Ana | ativa |
| 502 | 10 | Bruno | concluida |
| 503 | 20 | Caio | ativa |
Uma linha de trilha representa uma trilha. Uma linha de matricula representa uma matrícula de um estudante em uma trilha. Depois da junção, uma linha representa uma combinação trilha–matrícula.
Essa mudança é decisiva: Banco de Dados aparece duas vezes porque possui duas matrículas. Não há duplicata acidental; existem dois fatos distintos na granularidade do resultado.
ON decide quais pares correspondem
A condição mais comum compara uma chave estrangeira com a chave que ela referencia:
FROM trilha AS t
JOIN matricula AS m
ON m.trilha_id = t.trilha_id
Para cada par candidato, ON avalia uma expressão booleana. O par corresponde somente quando o resultado é TRUE. FALSE e UNKNOWN não formam uma correspondência.
Isso conecta a consulta ao projeto relacional: chaves primárias e estrangeiras materializam os relacionamentos, enquanto o JOIN escolhe como percorrê-los em uma pergunta específica.
A condição deve representar o relacionamento completo
Em uma relação identificada por duas colunas, comparar apenas uma pode combinar linhas indevidas:
-- Chave da turma: (curso_id, turma_codigo)
JOIN turma AS tu
ON tu.curso_id = inscricao.curso_id
AND tu.turma_codigo = inscricao.turma_codigo
O mesmo cuidado vale para dados particionados por organização, versão ou período. Não acrescente condições por hábito: compare exatamente os atributos que definem a correspondência no modelo.
NULL não corresponde a NULL com igualdade comum
ON a.codigo_externo = b.codigo_externo
Se um dos valores for NULL, a igualdade resulta em UNKNOWN; o par não corresponde. Dois nulos também não correspondem por =. Quando a regra realmente considera duas ausências equivalentes, alguns SGBDs oferecem predicados como IS NOT DISTINCT FROM, mas essa escolha precisa representar o domínio — não apenas aumentar a quantidade de resultados.
INNER JOIN mantém apenas correspondências
INNER JOIN retorna uma linha para cada par em que ON é verdadeiro:
SELECT
t.trilha_id,
t.titulo,
m.matricula_id,
m.estudante
FROM trilha AS t
INNER JOIN matricula AS m
ON m.trilha_id = t.trilha_id
ORDER BY t.trilha_id, m.matricula_id;
Resultado:
| trilha_id | titulo | matricula_id | estudante |
|---|---|---|---|
| 10 | Banco de Dados | 501 | Ana |
| 10 | Banco de Dados | 502 | Bruno |
| 20 | Redes | 503 | Caio |
Segurança não aparece porque não possui matrícula. INNER pode ser omitido — JOIN sozinho significa junção interna no PostgreSQL e no padrão usual —, mas escrevê-lo pode ajudar enquanto o leitor está aprendendo.
LEFT JOIN preserva todas as linhas da esquerda
LEFT JOIN primeiro produz as correspondências. Depois acrescenta uma linha para cada item da esquerda que não correspondeu, preenchendo com NULL as colunas do lado direito.
SELECT
t.trilha_id,
t.titulo,
m.matricula_id,
m.estudante
FROM trilha AS t
LEFT JOIN matricula AS m
ON m.trilha_id = t.trilha_id
ORDER BY t.trilha_id, m.matricula_id;
| trilha_id | titulo | matricula_id | estudante |
|---|---|---|---|
| 10 | Banco de Dados | 501 | Ana |
| 10 | Banco de Dados | 502 | Bruno |
| 20 | Redes | 503 | Caio |
| 30 | Segurança | NULL | NULL |
A linha de Segurança não afirma que existe uma matrícula com valores nulos. Esses nulos foram introduzidos pela junção para representar a ausência de correspondência.
O lado preservado aparece no nome: LEFT mantém ao menos uma linha de saída para cada linha da fonte escrita à esquerda. Isso não significa exatamente uma linha por item: se houver três correspondências à direita, haverá três resultados.
Encontrando linhas sem correspondência
Para listar trilhas sem matrículas:
SELECT t.trilha_id, t.titulo
FROM trilha AS t
LEFT JOIN matricula AS m
ON m.trilha_id = t.trilha_id
WHERE m.matricula_id IS NULL;
Teste uma coluna do lado direito que seja garantidamente não nula quando uma linha real existir, como a chave primária. Testar m.observacao IS NULL confundiria “não houve matrícula” com “houve matrícula, mas a observação é nula”.
Esse padrão é chamado de anti-join. A aula de consultas avançadas comparará essa forma com NOT EXISTS, especialmente quando a intenção é apenas testar existência.
RIGHT JOIN preserva todas as linhas da direita
RIGHT JOIN é o espelho de LEFT JOIN: preserva todas as linhas da fonte à direita e completa com nulos quando o lado esquerdo não corresponde.
SELECT
t.titulo,
i.codigo,
i.titulo_importado
FROM catalogo_atual AS t
RIGHT JOIN importacao AS i
ON i.codigo = t.codigo;
Ele é válido e pode ser útil quando a direção comunica o requisito. Ainda assim, muitas equipes preferem reordenar as fontes e usar LEFT JOIN:
FROM importacao AS i
LEFT JOIN catalogo_atual AS t
ON t.codigo = i.codigo
As duas formas podem expressar o mesmo resultado. Em cadeias longas, manter a leitura predominantemente da esquerda para a direita costuma reduzir confusão. Não existe ganho automático de desempenho por trocar RIGHT por LEFT; essa é uma decisão de clareza, e o otimizador escolhe o plano físico.
FULL JOIN preserva ausências dos dois lados
FULL OUTER JOIN reúne:
- os pares correspondentes;
- as linhas exclusivas da esquerda, com nulos à direita;
- as linhas exclusivas da direita, com nulos à esquerda.
Ele é especialmente útil para reconciliação entre fontes independentes. Imagine um catálogo atual e um arquivo de importação:
SELECT
COALESCE(c.codigo, i.codigo) AS codigo,
c.titulo AS titulo_atual,
i.titulo AS titulo_importado,
CASE
WHEN c.codigo IS NULL THEN 'somente_importacao'
WHEN i.codigo IS NULL THEN 'somente_catalogo'
ELSE 'nos_dois'
END AS origem
FROM catalogo_atual AS c
FULL JOIN importacao AS i
ON i.codigo = c.codigo
ORDER BY codigo;
Se SEG existir só no catálogo e CLOUD apenas no arquivo, ambos aparecem. COALESCE oferece um código de saída independentemente do lado existente.
CROSS JOIN produz todas as combinações
CROSS JOIN não possui condição ON. Cada linha da esquerda é combinada com cada linha da direita. Com N linhas de um lado e M do outro, o resultado contém N × M linhas.
SELECT
turno.nome AS turno,
modalidade.nome AS modalidade
FROM turno
CROSS JOIN modalidade
ORDER BY turno.nome, modalidade.nome;
Com três turnos e duas modalidades, surgem seis combinações. Isso pode ser exatamente o requisito ao gerar uma matriz de opções, calendário ou conjunto de cenários.
manhã × presencial
manhã × online
tarde × presencial
tarde × online
noite × presencial
noite × online
Escrever FROM a, b também forma um produto cartesiano, mas a sintaxe explícita comunica melhor a intenção. Ela ainda evita ambiguidades de precedência quando JOINs adicionais entram na cláusula FROM.
ON e WHERE respondem perguntas diferentes
Em um INNER JOIN, mover certos filtros entre ON e WHERE pode produzir o mesmo conjunto. Em um JOIN externo, pode mudar completamente quais linhas são preservadas.
Requisito: listar todas as trilhas e, quando existirem, suas matrículas ativas.
SELECT t.titulo, m.estudante
FROM trilha AS t
LEFT JOIN matricula AS m
ON m.trilha_id = t.trilha_id
AND m.situacao = 'ativa';
O filtro está em ON, portanto decide quais matrículas correspondem. A trilha continua preservada mesmo se nenhuma matrícula ativa existir.
Agora compare:
SELECT t.titulo, m.estudante
FROM trilha AS t
LEFT JOIN matricula AS m
ON m.trilha_id = t.trilha_id
WHERE m.situacao = 'ativa';
WHERE é aplicado depois do resultado do JOIN. Para trilhas sem correspondência, m.situacao é NULL; a comparação produz UNKNOWN, e a linha sai. Na prática, esse filtro elimina as linhas que o LEFT JOIN pretendia preservar.
Uma regra prática:
- condições que definem qual linha do lado opcional pode corresponder geralmente pertencem a
ON; - condições que definem quais linhas do resultado final devem permanecer pertencem a
WHERE.
Não aplique a regra mecanicamente. Leia o requisito e preveja o tratamento das ausências.
Multiplicidade explica o crescimento das linhas
Um JOIN não promete uma linha de saída para cada linha de entrada. A quantidade depende de quantas correspondências cada linha encontra.
| Relação lógica | Correspondências possíveis | Efeito típico no resultado |
|---|---|---|
| um para um | no máximo uma por linha | quantidade tende a permanecer |
| um para muitos | várias no lado “muitos” | a linha do lado “um” se repete |
| muitos para muitos | várias nos dois lados | combinações podem crescer rapidamente |
Se uma trilha tem três módulos e quatro matrículas, juntar trilha, modulo e matricula apenas por trilha_id produz até doze combinações para essa trilha: cada módulo é combinado com cada matrícula.
SELECT t.titulo, mo.titulo, ma.estudante
FROM trilha AS t
JOIN modulo AS mo
ON mo.trilha_id = t.trilha_id
JOIN matricula AS ma
ON ma.trilha_id = t.trilha_id;
Talvez doze linhas sejam corretas, caso cada linha represente módulo–matrícula. Se a pergunta era apenas “quantos módulos e quantas matrículas a trilha possui”, a granularidade foi misturada. Agregar cada lado antes de combinar ou usar subconsultas adequadas pode ser necessário; a próxima aula tratará GROUP BY e agregações.
DISTINCT não conserta uma granularidade errada
Adicionar DISTINCT pode ocultar parte da multiplicação, mas também pode fundir fatos legítimos que coincidem nas colunas selecionadas. Primeiro descubra:
- qual fato cada tabela representa;
- se a condição usa a chave completa;
- quantas correspondências são válidas por linha;
- qual deve ser a granularidade final.
Só então decida se eliminar duplicatas faz parte do requisito.
ON, USING e NATURAL JOIN
ON é a forma mais explícita
FROM trilha AS t
JOIN modulo AS m
ON m.trilha_id = t.trilha_id
ON aceita qualquer expressão booleana e mantém visíveis os dois nomes das colunas na saída de SELECT *.
USING reduz colunas repetidas
Quando as colunas de junção têm o mesmo nome nos dois lados:
FROM trilha
JOIN modulo USING (trilha_id)
USING (trilha_id) cria a comparação de igualdade e apresenta uma única coluna trilha_id no resultado da junção. É conciso, mas só serve quando a igualdade entre colunas homônimas representa exatamente a relação.
NATURAL JOIN é frágil diante do esquema
NATURAL JOIN usa automaticamente todos os nomes de colunas presentes nos dois lados. Se amanhã ambas as tabelas ganharem uma coluna situacao, a condição muda sem que a consulta seja editada. Se não houver nome comum, no PostgreSQL ele se comporta como um produto cartesiano.
Por isso, prefira ON ou USING com a lista explícita. A consulta deve registrar o relacionamento pretendido, não inferi-lo de coincidências atuais no esquema.
Self-join relaciona linhas da mesma tabela
Uma tabela pode desempenhar dois papéis na mesma consulta. Considere conteúdos com um pré-requisito opcional:
SELECT
atual.titulo AS conteudo,
anterior.titulo AS pre_requisito
FROM conteudo AS atual
LEFT JOIN conteudo AS anterior
ON anterior.conteudo_id = atual.pre_requisito_id
ORDER BY atual.ordem;
Não existe um tipo especial chamado self-join. É um JOIN comum com a mesma tabela referenciada duas vezes. Os aliases são obrigatórios para distinguir os papéis atual e anterior.
Encadeie JOINs com uma pergunta por etapa
SELECT
t.titulo AS trilha,
mo.titulo AS modulo,
c.titulo AS conteudo
FROM trilha AS t
JOIN modulo AS mo
ON mo.trilha_id = t.trilha_id
JOIN conteudo AS c
ON c.modulo_id = mo.modulo_id
ORDER BY t.titulo, mo.ordem, c.ordem;
Leia da esquerda para a direita:
- trilha corresponde a módulo pela chave da trilha;
- cada resultado intermediário corresponde a conteúdo pela chave do módulo;
- cada linha final representa trilha–módulo–conteúdo.
JOINs sem parênteses são associados da esquerda para a direita na sintaxe do PostgreSQL. O otimizador pode escolher outra estratégia física quando preservar a semântica. Use parênteses quando a estrutura lógica, especialmente com JOINs externos, não estiver clara; não tente controlar desempenho apenas pela ordem textual.
Escolha o JOIN a partir da pergunta
| Pergunta | Operação provável |
|---|---|
| Quais trilhas possuem matrículas? | INNER JOIN |
| Todas as trilhas e suas matrículas, se houver | LEFT JOIN |
| Todos os registros importados e seus equivalentes locais | RIGHT JOIN ou fontes invertidas com LEFT JOIN |
| Quais itens existem em qualquer uma das duas fontes? | FULL JOIN |
| Quais combinações de turno e modalidade podem ser oferecidas? | CROSS JOIN |
| Quais trilhas não possuem matrícula? | LEFT JOIN + teste de chave nula, ou anti-join equivalente |
Essa tabela indica um ponto de partida, não uma receita universal. O lado preservado, a granularidade e a condição completa ainda precisam ser definidos.
Erros comuns
- esquecer parte da chave composta: pares que não representam o relacionamento são combinados;
- omitir a condição sem intenção: surge um produto cartesiano potencialmente enorme;
- esperar uma linha por entidade: relações um-para-muitos repetem legitimamente os dados do lado “um”;
- usar
DISTINCTcomo correção automática: o sintoma some sem explicar a granularidade; - filtrar o lado opcional em
WHERE: umLEFT JOINpode perder as linhas preservadas; - testar uma coluna anulável para encontrar ausências: uma linha real é confundida com falta de correspondência;
- supor que dois nulos correspondem por igualdade:
NULL = NULLresulta emUNKNOWN; - usar
NATURAL JOINpor conveniência: uma alteração no esquema pode mudar a condição; - misturar vírgulas e JOINs explícitos: precedência e escopo dos nomes ficam menos claros;
- imaginar que RIGHT JOIN é mais rápido ou mais lento por direção: o tipo expressa semântica; desempenho depende do plano e dos dados;
- atribuir significado aos nulos introduzidos pelo JOIN como se estivessem armazenados: eles representam a ausência do lado não preservado.
Checklist antes de executar
- O que cada linha de cada fonte representa?
- O que cada linha do resultado deverá representar?
- Quais colunas formam a condição completa de correspondência?
- A condição pode produzir
NULLouUNKNOWN? - Qual lado precisa permanecer quando não houver correspondência?
- Quantas correspondências válidas cada linha pode encontrar?
- Um filtro em
WHEREremoverá linhas introduzidas por um JOIN externo? - O teste de ausência usa uma coluna garantidamente não nula?
- O crescimento esperado é aditivo, repetitivo ou cartesiano?
DISTINCTé requisito do resultado ou tentativa de esconder um erro?- Os aliases identificam papéis, especialmente em self-joins?
- A ordem final foi declarada com
ORDER BYquando necessária?
O que você deve guardar
Todo JOIN responde a três perguntas: quando duas linhas correspondem, quais lados são preservados e qual fato cada linha resultante representa.
INNER JOIN mantém correspondências; LEFT preserva também a esquerda; RIGHT, a direita; FULL, os dois lados; CROSS JOIN cria todas as combinações. ON participa da correspondência, enquanto WHERE filtra o resultado já construído. A multiplicidade explica por que linhas crescem — e deve ser compreendida antes de qualquer DISTINCT.
Na próxima aula, usaremos agregações, GROUP BY e HAVING para transformar várias linhas em resumos coerentes sem perder o controle da granularidade.
Referências
- ISO — ISO/IEC 9075-2:2023, SQL/Foundation. Acesso em 30 ago. 2026.
- ISO — ISO/IEC 9075-2:2023/Cor 1:2026. Acesso em 30 ago. 2026.
- PostgreSQL Global Development Group — Table Expressions: Joined Tables. Acesso em 30 ago. 2026.
- PostgreSQL Global Development Group — Joins Between Tables. Acesso em 30 ago. 2026.
- PostgreSQL Global Development Group — Comparison Functions and Operators. Acesso em 30 ago. 2026.
- PostgreSQL Global Development Group — The WHERE Clause. Acesso em 30 ago. 2026.
