sqlpostgresqljoinsnull

Anti-joins em SQL: encontrar linhas sem correspondência

Três formas de encontrar linhas sem correspondência — LEFT JOIN / IS NULL, NOT EXISTS e NOT IN — e por que NOT IN quebra com NULL.

4 min de leituraReferencesql · postgresql · joins · null · anti-join

Um anti-join responde a uma única pergunta: "quais linhas da tabela A não têm correspondência na tabela B?" Usuários sem pedidos, pedidos sem pagamento, funcionários sem gestor. O SQL não tem uma palavra-chave ANTI JOIN, então você o expressa de uma de três maneiras clássicas. As três retornam a resposta "certa" com dados limpos, mas divergem fortemente assim que NULL entra em cena. Vamos percorrê-las sobre um único esquema e descobrir qual delas escolher.

Vamos usar estas tabelas (PostgreSQL):

CREATE TABLE users (
  id    bigint PRIMARY KEY,
  email text NOT NULL
);

CREATE TABLE orders (
  id       bigint PRIMARY KEY,
  user_id  bigint,          -- nullable: a "guest" order
  amount   numeric NOT NULL
);

Tarefa em andamento: encontrar usuários que não fizeram nenhum pedido.

LEFT JOIN ... WHERE b.id IS NULL

A abordagem mais visual. Faça um LEFT JOIN que mantenha cada linha de users e depois descarte as que encontraram correspondência.

SELECT u.id, u.email
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;   -- no match => the whole right side is NULL

Como ler: o LEFT JOIN preenche com NULL todas as colunas de orders quando não há correspondência. O filtro WHERE o.id IS NULL mantém exatamente essas linhas sem correspondência. O detalhe-chave: teste IS NULL em uma coluna que nunca seja NULL em uma linha real — a chave primária o.id é perfeita. Se você testar o.user_id (que aqui aceita nulos), pode capturar acidentalmente pedidos de convidados e obter lixo.

  • A favor: intuitivo, e é fácil trazer colunas extras de orders para depuração.
  • Contra: a semântica de "apague o que combinou" não fica óbvia para quem ler a consulta depois.
  • Pegadinha: a correspondência por várias colunas deixa a condição IS NULL prolixa e frágil.

NOT EXISTS — a forma recomendada

NOT EXISTS com uma subconsulta correlacionada declara a intenção literalmente: "não há nenhum pedido para este usuário".

SELECT u.id, u.email
FROM users u
WHERE NOT EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.user_id = u.id
);

Por que essa costuma ser a melhor escolha:

  • Lê-se como o requisito. "Não existe esse pedido." Pronto.
  • Seguro com NULL. EXISTS só verifica se existem linhas; não compara valores de volta com a consulta externa. Um NULL em o.user_id simplesmente não satisfaz o.user_id = u.id e conta como ausência de correspondência — sem surpresas.
  • Desempenho. O planejador do PostgreSQL executa tanto NOT EXISTS quanto LEFT JOIN / IS NULL como o mesmo anti join físico (você verá Hash Anti Join no EXPLAIN). Na prática são equivalentes em velocidade; NOT EXISTS sai na frente com chaves compostas.

Uma chave composta permanece limpa — sem verificações IS NULL espalhadas:

WHERE NOT EXISTS (
  SELECT 1 FROM orders o
  WHERE o.user_id = u.id AND o.region = u.region
)

NOT IN e a armadilha do NULL

NOT IN parece compacto e tentador — e é o mais traiçoeiro dos três.

-- DANGEROUS if orders.user_id contains NULL
SELECT u.id, u.email
FROM users u
WHERE u.id NOT IN (SELECT user_id FROM orders);

Se a subconsulta produzir um único NULL que seja (e aqui user_id aceita nulos — pedidos de convidados!), a consulta retorna zero linhas. Isso não é um bug do banco de dados; é a lógica de três valores do SQL. x NOT IN (1, 2, NULL) se expande para x <> 1 AND x <> 2 AND x <> NULL. Essa última comparação é avaliada como UNKNOWN, então a expressão inteira nunca pode se tornar TRUE — a linha é descartada silenciosamente.

  • Pegadinha: a falha é silenciosa. A consulta não dá erro; ela apenas devolve um resultado vazio (ou truncado).
  • Se você tem certeza de que não há NULL, adicione um filtro explícito — mas é fácil esquecer quando os dados mudam:
WHERE u.id NOT IN (
  SELECT user_id FROM orders WHERE user_id IS NOT NULL
);

Notas entre motores:

  • MySQL se comporta de forma idêntica — a mesma lógica de três valores, a mesma explosão com NULL.
  • ClickHouse também é afetado; além disso, seu JOIN tem peculiaridades próprias, então NOT IN sobre uma coluna garantidamente sem NULL (ou LEFT ANTI JOIN) costuma ser mais limpo. Vale notar que o ClickHouse é um dos poucos motores com uma sintaxe explícita LEFT ANTI JOIN.

Qual escolher

A regra curta:

  • Use NOT EXISTS por padrão. Expressivo, seguro com NULL, e o otimizador o transforma em um anti join. Faça dele sua escolha principal.
  • LEFT JOIN ... IS NULL — quando você também quiser colunas da tabela da direita, ou simplesmente achar a leitura mais clara. Sempre teste IS NULL em uma chave que não aceite nulos.
  • NOT IN — só quando a coluna da subconsulta tiver garantia de ser NOT NULL (por exemplo, uma chave primária). Caso contrário, filtre os NULL explicitamente, ou simplesmente não use.

Checklist final: a subconsulta pode retornar NULL? Se sim, nada de NOT IN. Precisa de campos do lado direito? Use LEFT JOIN. Em todos os outros casos, escreva NOT EXISTS e esqueça que a lógica de três valores existe.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador