CEVIU Logo
Voltar
Cuidado com NULL em NOT IN: o erro silencioso que quebra suas consultas no PostgreSQL

Cuidado com NULL em NOT IN: o erro silencioso que quebra suas consultas no PostgreSQL

Aprofundamento CEVIU

Aprofundamento

O comportamento de NOT IN com NULL não é um bug do PostgreSQL, mas uma consequência direta da lógica de três valores (TRUE/FALSE/UNKNOWN) do SQL padrão, e o parser do Postgres aplica isso com precisão cirúrgica. Quando NOT IN é usado com subconsulta, o parser o transforma internamente em NOT (col = ANY(subquery)), e não em uma cadeia de <> ALL como na forma literal. Isso faz com que qualquer NULL no lado direito ou esquerdo torne a comparação UNKNOWN, descartando todas as linhas. A armadilha é silenciosa porque o resultado é sintaticamente válido, sem erros ou avisos.

O PostgreSQL 19 (beta desde 4/6/2026, versão final prevista para setembro) traz uma mudança estrutural no planejador: agora ele converte NOT IN em Hash Anti Join, mas só quando prova que NENHUM dos operandos pode ser NULL. Essa prova depende de três condições simultâneas: colunas marcadas como NOT NULL no esquema, rastreamento de nullabilidade em joins externos e uso de operadores B-tree/Hash (como =). Se falhar em qualquer uma, o plano volta ao SubPlan opaco, exatamente como nas versões anteriores. Ou seja: a otimização não resolve o problema lógico, só acelera os casos já seguros.

O que mudou

Antes do PostgreSQL 19, NOT IN com subconsulta nunca era convertido em anti-join, mesmo com colunas NOT NULL. O planejador simplesmente não verificava a nullabilidade. Agora, com a função sublink_testexpr_is_not_nullable (introduzida no commit 383eb21ebff, março/2026), o otimizador reconhece explicitamente quando NULL está ausente e ativa a conversão. É a primeira vez em 17 anos (desde o anti-join no PostgreSQL 8.4, 2009) que o planner trata NOT IN como um join de primeira classe, mas apenas sob garantia formal, não por inferência ou configuração.

Por que isso importa

Para engenheiros de dados, isso não é só sobre performance: é sobre previsibilidade. Um pipeline que usa NOT IN em uma view com coluna nullable pode funcionar por meses e quebrar do nada ao inserir um único NULL, sem log, sem alerta, só dados sumindo. Já o NOT EXISTS é determinístico: mantém linhas com NULL no lado esquerdo, pois pergunta 'existe correspondência?', não 'é diferente de todos?'. E com o PostgreSQL 19, quem já modelou colunas como NOT NULL ganha ganho de desempenho imediato, sem mudar código, enquanto quem usa NULLs continua exposto à mesma armadilha de 1996.

Linha do tempo

  1. PostgreSQL 8.4 introduz anti-joins, mas NOT IN continua sem conversão devido à incompatibilidade com NULL

  2. Commit 383eb21ebff adiciona sublink_testexpr_is_not_nullable, permitindo conversão segura de NOT IN em anti-join

  3. Lançamento do PostgreSQL 19 Beta 1 com suporte experimental a NOT IN como anti-join em cenários NOT NULL

  4. Publicação da análise sobre o comportamento silencioso de NOT IN com NULL e detalhamento da nova otimização

Perguntas frequentes

NOT IN com lista de valores (NOT IN (1, 2, NULL)) também sofre do mesmo problema?

Sim, e de forma ainda mais direta. O parser expande NOT IN (1, 2, NULL) para (col <> 1) AND (col <> 2) AND (col <> NULL). Como a última comparação sempre retorna UNKNOWN, toda a expressão vira UNKNOWN, e a linha é descartada. Não há otimização de anti-join nesse caso: é pura lógica de três valores.

Se eu usar COALESCE na subconsulta, como NOT IN (SELECT COALESCE(col, -1) FROM t), resolvo o problema?

Não resolve, e piora. Você troca um NULL por um valor arbitrário (-1), mas agora a lógica da consulta muda: linhas cujo valor real era NULL passam a ser comparadas com -1. Isso gera resultados incorretos, não apenas vazios. A solução correta é filtrar com WHERE col IS NOT NULL na subconsulta ou migrar para NOT EXISTS.

O PostgreSQL 19 corrige o comportamento lógico de NOT IN com NULL?

Não. A semântica continua idêntica ao padrão SQL: qualquer NULL envolvido faz a cláusula retornar UNKNOWN e descartar a linha. A mudança é exclusivamente no planejador, e só para os casos onde NULL é provado impossível. A armadilha lógica persiste para colunas nullable, exatamente como antes.

Posso confiar em EXCEPT como substituto universal para NOT IN?

Não. EXCEPT usa IS NOT DISTINCT FROM, então dois NULLs se igualam, o que pode manter ou descartar linhas com NULL dependendo do contexto. Por exemplo, se sua subconsulta tem NULL, EXCEPT remove todas as linhas NULL da tabela principal. Já NOT EXISTS preserva essas linhas. Escolha conforme a intenção: 'não há correspondência' (NOT EXISTS) ou 'não pertence ao conjunto' (EXCEPT).

Fontes

Avalie este artigo:
Compartilhar:
Categoria
CEVIU Dados
Publicado
18 de junho de 2026
Editoria
CEVIU Dados

Quer receber mais sobre CEVIU Dados?

Conteúdo curado diariamente, direto no seu e-mail.

Conteúdo curado diariamenteDiversas categoriasCancele quando quiser