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,
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;
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 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.
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.
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 queNULLentra 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 JOINque mantenha cada linha deuserse 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 NULLComo ler: o
LEFT JOINpreenche comNULLtodas as colunas deordersquando não há correspondência. O filtroWHERE o.id IS NULLmantém exatamente essas linhas sem correspondência. O detalhe-chave: testeIS NULLem uma coluna que nunca sejaNULLem uma linha real — a chave primáriao.idé perfeita. Se você testaro.user_id(que aqui aceita nulos), pode capturar acidentalmente pedidos de convidados e obter lixo.orderspara depuração.IS NULLprolixa e frágil.NOT EXISTS — a forma recomendada
NOT EXISTScom 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:
EXISTSsó verifica se existem linhas; não compara valores de volta com a consulta externa. UmNULLemo.user_idsimplesmente não satisfazo.user_id = u.ide conta como ausência de correspondência — sem surpresas.NOT EXISTSquantoLEFT JOIN / IS NULLcomo o mesmo anti join físico (você veráHash Anti JoinnoEXPLAIN). Na prática são equivalentes em velocidade;NOT EXISTSsai na frente com chaves compostas.Uma chave composta permanece limpa — sem verificações
IS NULLespalhadas: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 INparece 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
NULLque seja (e aquiuser_idaceita 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 parax <> 1 AND x <> 2 AND x <> NULL. Essa última comparação é avaliada comoUNKNOWN, então a expressão inteira nunca pode se tornarTRUE— a linha é descartada silenciosamente.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:
NULL.JOINtem peculiaridades próprias, entãoNOT INsobre uma coluna garantidamente semNULL(ouLEFT ANTI JOIN) costuma ser mais limpo. Vale notar que o ClickHouse é um dos poucos motores com uma sintaxe explícitaLEFT ANTI JOIN.Qual escolher
A regra curta:
NOT EXISTSpor 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 testeIS NULLem uma chave que não aceite nulos.NOT IN— só quando a coluna da subconsulta tiver garantia de serNOT NULL(por exemplo, uma chave primária). Caso contrário, filtre osNULLexplicitamente, ou simplesmente não use.Checklist final: a subconsulta pode retornar
NULL? Se sim, nada deNOT IN. Precisa de campos do lado direito? UseLEFT JOIN. Em todos os outros casos, escrevaNOT EXISTSe esqueça que a lógica de três valores existe.