JOINs de SQL explicados de forma visual
INNER, LEFT, RIGHT, FULL e CROSS JOIN explicados com exemplos concretos de tabelas, condições de junção e as armadilhas que multiplicam ou somem linhas.
Neste artigo
Bancos relacionais espalham os dados por várias tabelas de propósito. O cliente numa tabela, os pedidos em outra, os itens de cada pedido em uma terceira. Essa separação mantém cada fato num lugar só e evita inconsistência, mas cobra um preço na hora de ler: para montar uma visão completa — "quais clientes fizeram quais pedidos" — você precisa recombinar as tabelas. O JOIN é a operação que faz essa recombinação, e entendê-lo bem é a diferença entre consultas que respondem exatamente o que você quer e consultas que somem com linhas ou as multiplicam misteriosamente.
A ideia central de um JOIN é simples: você diz ao banco quais duas tabelas quer combinar e qual é a condição de junção, ou seja, a regra que decide quais linhas de uma casam com quais linhas da outra. Na esmagadora maioria dos casos, a condição compara uma chave estrangeira de uma tabela com a chave primária da outra — por exemplo, pedidos.cliente_id = clientes.id. O banco então examina as combinações possíveis e monta as linhas do resultado segundo o tipo de JOIN que você escolheu.
O que muda entre os tipos de JOIN é o tratamento das linhas que não têm par do outro lado. É aí que mora toda a confusão e todo o poder. Vamos construir uma intuição visual, tipo por tipo, com duas tabelas de exemplo: clientes, com clientes que podem ou não ter pedidos, e pedidos, que podem ou não apontar para um cliente válido.
INNER JOIN: só o que casa dos dois lados#
O INNER JOIN retorna apenas as linhas que têm correspondência em ambas as tabelas. Se um cliente não tem nenhum pedido, ele não aparece. Se um pedido, por algum motivo, não aponta para um cliente existente, ele também não aparece. É a interseção.
Visualize dois círculos que se sobrepõem, como um diagrama de Venn. O INNER JOIN devolve só a região central, onde os dois se cruzam. Escrevendo SELECT c.nome, p.valor FROM clientes c INNER JOIN pedidos p ON p.cliente_id = c.id, você obtém uma linha para cada par cliente-pedido que efetivamente existe. Um cliente com três pedidos aparece três vezes, uma para cada pedido; um cliente sem pedidos simplesmente não entra no resultado.
Esse é o JOIN mais comum e o comportamento que a maioria das pessoas espera por padrão. A palavra INNER inclusive é opcional na maioria dos bancos: escrever só JOIN já significa INNER JOIN. Use-o sempre que você só se interessa por registros que têm ambos os lados presentes — clientes que compraram, pedidos com cliente válido, itens que pertencem a um produto conhecido.
O risco silencioso do INNER JOIN é justamente o que ele descarta. Se você quer contar todos os clientes e por engano usa INNER JOIN com pedidos, os clientes sem pedido somem da contagem, e o número vem menor do que deveria — sem nenhum erro, só um resultado incompleto. Sempre pergunte: eu quero excluir quem não tem par do outro lado? Se a resposta for não, você precisa de um OUTER JOIN.
LEFT JOIN: tudo da esquerda, o que casar da direita#
O LEFT JOIN (ou LEFT OUTER JOIN) retorna todas as linhas da tabela da esquerda, e para cada uma traz os dados correspondentes da direita — ou NULL quando não há correspondência.
Esse é provavelmente o JOIN mais útil no dia a dia depois do INNER, porque responde à pergunta "me dê todos os X, e o Y de cada um quando existir". Em SELECT c.nome, p.valor FROM clientes c LEFT JOIN pedidos p ON p.cliente_id = c.id, todos os clientes aparecem. Os que têm pedidos aparecem uma vez por pedido, com os valores preenchidos. Os que não têm pedido nenhum aparecem uma vez só, com a coluna p.valor vindo como NULL.
No diagrama de Venn, o LEFT JOIN é o círculo da esquerda inteiro — a interseção mais a parte que só existe à esquerda. É a ferramenta certa para relatórios de completude: quais clientes nunca compraram, quais produtos nunca foram vendidos, quais usuários nunca fizeram login. Para achar esses casos, você faz o LEFT JOIN e filtra por WHERE p.id IS NULL — o que sobra são exatamente as linhas da esquerda que não encontraram par.
Aqui mora uma armadilha clássica e sutil. Se você faz um LEFT JOIN e depois adiciona no WHERE uma condição sobre uma coluna da tabela da direita — algo como WHERE p.valor > 100 — você anula silenciosamente o efeito do LEFT. As linhas sem par têm p.valor igual a NULL, e a comparação NULL > 100 não é verdadeira, então essas linhas são filtradas para fora, transformando na prática o LEFT JOIN em INNER JOIN. Se a intenção era manter os clientes sem pedido, a condição sobre a tabela da direita precisa ir para dentro da cláusula ON, não no WHERE. Essa distinção entre filtrar na junção e filtrar depois da junção é uma das causas mais comuns de resultados errados.
RIGHT JOIN: o espelho do LEFT#
O RIGHT JOIN faz exatamente o que o LEFT JOIN faz, mas invertendo os papéis: traz todas as linhas da tabela da direita e o que casar da esquerda, com NULL onde não há par.
Na prática, o RIGHT JOIN é pouco usado, porque qualquer RIGHT JOIN pode ser reescrito como um LEFT JOIN apenas trocando a ordem das tabelas. A RIGHT JOIN B é o mesmo que B LEFT JOIN A. Como a maioria das pessoas raciocina da esquerda para a direita, e como o LEFT JOIN deixa a tabela "principal" à esquerda de forma natural, os times costumam padronizar em LEFT JOIN por legibilidade. Conhecer o RIGHT JOIN é importante para ler código alheio, mas ao escrever o seu, prefira o LEFT e mantenha a tabela dominante à esquerda.
FULL JOIN: tudo dos dois lados#
O FULL JOIN (ou FULL OUTER JOIN) retorna todas as linhas de ambas as tabelas, casando o que puder e preenchendo com NULL os lados sem correspondência.
É a união completa dos dois círculos do Venn: a interseção, mais o que só existe à esquerda, mais o que só existe à direita. Os clientes sem pedido aparecem com as colunas de pedido em NULL; os pedidos sem cliente válido aparecem com as colunas de cliente em NULL; e os que casam aparecem preenchidos.
O FULL JOIN é o menos frequente no dia a dia, mas brilha em tarefas de reconciliação e auditoria: comparar dois conjuntos e encontrar tudo que existe de um lado mas não do outro, em ambas as direções ao mesmo tempo. Um caso típico é confrontar registros de dois sistemas que deveriam estar sincronizados — o FULL JOIN seguido de um filtro que busca NULL de qualquer um dos lados revela exatamente o que está faltando em cada sistema. Nem todos os bancos suportam FULL JOIN nativamente, mas onde ele existe, é a ferramenta mais direta para esse tipo de comparação simétrica.
CROSS JOIN: todas as combinações possíveis#
O CROSS JOIN não tem condição de junção: ele combina cada linha da primeira tabela com cada linha da segunda, produzindo o produto cartesiano. Se uma tabela tem dez linhas e a outra tem vinte, o resultado tem duzentas linhas.
Diferente dos outros, o CROSS JOIN raramente é o que você quer de propósito — e é justamente o que você obtém por acidente quando esquece a condição de junção. Escrever SELECT ... FROM clientes, pedidos sem uma cláusula que relacione as duas tabelas gera o produto cartesiano completo, uma explosão de linhas sem sentido que casa cada cliente com cada pedido de todo mundo. Muitas consultas absurdamente lentas e com contagens infladas nascem exatamente desse esquecimento.
Há usos legítimos, no entanto. O CROSS JOIN é a forma correta de gerar todas as combinações quando você realmente quer todas elas: cruzar cada produto com cada tamanho disponível para montar uma grade de variações, ou cruzar uma tabela de datas com uma tabela de lojas para garantir uma linha por dia por loja mesmo nos dias sem venda. Quando o produto cartesiano é a intenção, declare CROSS JOIN explicitamente — isso deixa claro para quem lê que a multiplicação de linhas é deliberada, não um bug.
A armadilha que atravessa todos: a granularidade#
Independente do tipo, existe uma confusão que derruba tanto iniciante quanto veterano: a multiplicação de linhas por junções em cadeia. Quando você junta clientes com pedidos e depois com itens_pedido, o resultado tem uma linha para cada item de cada pedido de cada cliente. Se você então tentar somar o valor do pedido, vai somá-lo repetido uma vez para cada item — e o total virá inflado.
O problema é de granularidade: cada JOIN para o lado "muitos" de um relacionamento aumenta o número de linhas, e agregações feitas depois disso contam valores repetidos. A defesa é raciocinar sempre sobre qual é a granularidade do resultado — o que representa uma linha? — antes de agregar. Muitas vezes a solução é agregar cada nível separadamente, com subconsultas ou tabelas derivadas, para que a soma dos pedidos aconteça na granularidade de pedido, e só depois seja combinada com os clientes. Somar às cegas sobre um resultado de múltiplos JOIN é uma das causas mais frequentes de números errados em relatórios.
Um método para não errar#
Antes de escrever a agregação, descreva em uma frase o que uma linha do seu JOIN significa. "Uma linha por item de pedido." Se essa frase não bate com o que você quer contar ou somar, você já sabe que precisa ajustar a granularidade antes de agregar. Esse hábito simples previne a maioria dos erros de JOIN, porque força você a enxergar a multiplicação de linhas antes que ela contamine o resultado.
Fechando o raciocínio#
Os tipos de JOIN se distinguem por uma única pergunta: o que fazer com as linhas que não têm par do outro lado? O INNER JOIN as descarta. O LEFT JOIN preserva as da esquerda. O RIGHT JOIN preserva as da direita. O FULL JOIN preserva as duas. O CROSS JOIN ignora a ideia de par e combina tudo com tudo. Visualizar cada um como uma região de um diagrama de Venn transforma uma lista de palavras-chave numa intuição que você carrega para sempre.
Dominar JOIN não é decorar sintaxe; é raciocinar sobre inclusão, exclusão e granularidade. Saber onde colocar a condição — no ON ou no WHERE — e saber prever quantas linhas cada junção produz é o que separa uma consulta que responde a verdade de uma que produz números plausíveis mas errados. E números plausíveis e errados são muito mais perigosos do que um erro que estoura na cara, porque ninguém desconfia deles.