Capa do Ebook gratuito SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

SQL para Análise de Dados no Dia a Dia: Consultas, Relatórios e Insights com Dados Reais

Novo curso

26 páginas

Filtragem correta, tratamento de nulos e armadilhas comuns em condições

Capítulo 3

Tempo estimado de leitura: 0 minutos

+ Exercício

Por que “filtrar corretamente” é mais difícil do que parece

Em análises do dia a dia, a maior parte dos erros não vem de cálculos complexos, mas de condições mal formuladas: filtros que excluem registros sem querer, comparações que falham por causa de NULL, combinações de AND/OR com precedência inesperada, e armadilhas com NOT IN, BETWEEN e datas. O resultado costuma ser um relatório “quase certo”, mas com números que não batem com o operacional.

Neste capítulo, o foco é aprender a escrever condições robustas e previsíveis, entendendo como o SQL avalia expressões lógicas e como lidar com valores ausentes. A ideia é reduzir discrepâncias e tornar os filtros auditáveis.

Entendendo o comportamento de NULL: a base de quase todas as armadilhas

NULL não é zero, não é vazio e não é “falso”

NULL representa “desconhecido” ou “não informado”. Isso muda completamente a lógica: qualquer comparação com NULL (como =, <>, >, <) não retorna verdadeiro nem falso; retorna desconhecido. Em SQL, isso é chamado de lógica de três valores: TRUE, FALSE e UNKNOWN.

Na prática, em um WHERE, apenas condições avaliadas como TRUE passam. FALSE e UNKNOWN são descartadas. Esse detalhe explica por que filtros “óbvios” podem eliminar linhas com NULL sem você perceber.

Como testar NULL corretamente

Para verificar ausência/presença de valor, use:

Continue em nosso aplicativo

Você poderá ouvir o audiobook com a tela desligada, ganhar gratuitamente o certificado deste curso e ainda ter acesso a outros 5.000 cursos online gratuitos.

ou continue lendo abaixo...
Download App

Baixar o aplicativo

  • IS NULL
  • IS NOT NULL

Exemplo: encontrar pedidos sem data de envio registrada:

SELECT order_id, shipped_at FROM orders WHERE shipped_at IS NULL;

Evite padrões incorretos como:

-- Errado: nunca será TRUE para NULL (vira UNKNOWN) SELECT order_id FROM orders WHERE shipped_at = NULL;

NULL em expressões e funções: COALESCE e NULLIF

Dois recursos ajudam muito a tornar condições mais explícitas:

  • COALESCE(a, b, c): retorna o primeiro valor não nulo.
  • NULLIF(x, y): retorna NULL se x = y, senão retorna x.

Exemplo: tratar “string vazia” como nulo antes de filtrar:

SELECT customer_id, email FROM customers WHERE NULLIF(TRIM(email), '') IS NULL;

Exemplo: substituir nulo por um valor padrão para uma comparação (com cuidado para não distorcer o significado):

SELECT ticket_id, priority FROM tickets WHERE COALESCE(priority, 'unknown') = 'high';

Esse padrão é útil quando o negócio define explicitamente que nulo deve ser tratado como “unknown” e você quer incluir/excluir esse grupo de forma controlada.

Lógica booleana no SQL: precedência e parênteses

AND e OR não têm o mesmo “peso”

Em SQL, AND é avaliado antes de OR. Isso significa que:

-- Sem parênteses SELECT * FROM orders WHERE status = 'paid' OR status = 'shipped' AND total > 100;

É interpretado como:

status = 'paid' OR (status = 'shipped' AND total > 100)

Se a intenção era “(paid ou shipped) e total > 100”, você precisa explicitar:

SELECT * FROM orders WHERE (status = 'paid' OR status = 'shipped') AND total > 100;

Passo a passo para evitar bugs de precedência

  • Escreva a regra de negócio em português, com agrupamentos claros.
  • Traduza para SQL colocando parênteses em cada agrupamento.
  • Leia a condição final como uma frase, conferindo se cada parêntese corresponde a uma parte da regra.
  • Se possível, valide com uma amostra: conte quantos registros entram em cada grupo.

Exemplo de validação por partes (útil para auditoria):

SELECT COUNT(*) AS paid FROM orders WHERE status = 'paid'; SELECT COUNT(*) AS shipped_over_100 FROM orders WHERE status = 'shipped' AND total > 100; SELECT COUNT(*) AS final_rule FROM orders WHERE (status = 'paid' OR status = 'shipped') AND total > 100;

Armadilhas com NOT IN, IN e NULL

NOT IN + NULL pode zerar seu resultado

Essa é uma das armadilhas mais comuns. Se a lista do NOT IN contiver NULL, a condição pode virar UNKNOWN para todas as linhas, resultando em zero registros.

Exemplo: você quer clientes que não estão na lista de bloqueados:

SELECT c.customer_id FROM customers c WHERE c.customer_id NOT IN (SELECT b.customer_id FROM blocked_customers b);

Se blocked_customers.customer_id tiver algum NULL, o NOT IN pode falhar de forma silenciosa.

Como corrigir: filtrar NULL na subquery ou usar NOT EXISTS

Opção 1: remover nulos na subquery:

SELECT c.customer_id FROM customers c WHERE c.customer_id NOT IN (SELECT b.customer_id FROM blocked_customers b WHERE b.customer_id IS NOT NULL);

Opção 2 (geralmente mais robusta): usar NOT EXISTS:

SELECT c.customer_id FROM customers c WHERE NOT EXISTS (SELECT 1 FROM blocked_customers b WHERE b.customer_id = c.customer_id);

NOT EXISTS lida melhor com NULL porque a comparação é feita linha a linha e a existência (ou não) do match é o que importa.

IN com NULL: não “inclui” nulos

col IN (1, 2, NULL) não significa “1, 2 ou nulo”. Para incluir nulos, você precisa explicitar:

WHERE col IN (1, 2) OR col IS NULL

Comparações com strings: espaços, caixa e valores “quase iguais”

Espaços à esquerda/direita e strings vazias

Dados operacionais frequentemente têm inconsistências: 'SP', 'SP ', ' sp'. Se você filtrar diretamente, pode perder registros.

Exemplo de normalização no filtro:

SELECT * FROM customers WHERE UPPER(TRIM(state)) = 'SP';

Para identificar problemas de qualidade, você pode procurar valores “suspeitos”:

SELECT state, COUNT(*) FROM customers GROUP BY state ORDER BY COUNT(*) DESC;

Se aparecerem variações, normalize ou trate na camada de transformação, mas quando estiver filtrando para um relatório, deixe explícito no SQL o que está sendo feito (por exemplo, TRIM e UPPER).

LIKE, curingas e caracteres especiais

LIKE é útil, mas pode trazer falsos positivos. Por exemplo, filtrar produtos com “PRO” pode pegar “IMPROVÁVEL”. Prefira padrões mais específicos quando possível.

SELECT * FROM products WHERE product_name LIKE 'PRO%';

Se você precisa procurar um caractere literal que também é curinga (como % ou _), alguns bancos suportam ESCAPE, mas a sintaxe varia. Em cenários de relatórios, uma alternativa é evitar depender de padrões ambíguos e usar chaves/códigos sempre que existirem.

BETWEEN: inclusivo e perigoso em datas e valores contínuos

BETWEEN inclui as extremidades

BETWEEN a AND b equivale a >= a AND <= b. Isso é ótimo para faixas discretas (como inteiros), mas pode causar surpresas com datas/horários.

Armadilha clássica: filtrar um dia inteiro com timestamp

Suponha uma coluna created_at com data e hora. Se você fizer:

SELECT COUNT(*) FROM orders WHERE created_at BETWEEN '2026-01-01' AND '2026-01-31';

Dependendo do tipo e conversão do banco, '2026-01-31' pode ser interpretado como 2026-01-31 00:00:00, excluindo pedidos do dia 31 após meia-noite.

Padrão recomendado: intervalo semiaberto

Um padrão mais seguro é usar >= no início e < no início do próximo período:

SELECT COUNT(*) FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01';

Esse padrão evita problemas de “último segundo do dia” e funciona bem com timestamps.

Condições com datas: fuso, conversões e funções que quebram índices

Evite aplicar função na coluna quando possível

Um erro comum é transformar a coluna para comparar, por exemplo:

-- Pode ser lento e pode impedir uso de índice SELECT * FROM orders WHERE DATE(created_at) = '2026-01-01';

Além de potencialmente degradar performance, isso pode mascarar problemas de fuso horário e conversões.

Prefira comparar por intervalo:

SELECT * FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02';

Passo a passo para filtros de período confiáveis

  • Confirme se a coluna é DATE ou TIMESTAMP.
  • Defina o período como intervalo semiaberto (>= início e < próximo_início).
  • Se houver fuso horário, padronize o período no mesmo fuso do dado (ou converta explicitamente, se o banco suportar).
  • Teste com registros de borda (ex.: exatamente meia-noite, último minuto do mês).

Condições com números: arredondamento, ponto flutuante e comparações exatas

Evite igualdade em ponto flutuante

Se uma coluna é do tipo float/double, comparações exatas podem falhar por representação binária. Em análises, isso aparece quando você filtra por um valor calculado.

Em vez de:

WHERE metric = 0.1

Use tolerância (quando fizer sentido):

WHERE metric BETWEEN 0.0999 AND 0.1001

Ou arredonde para uma casa definida (ciente do impacto):

WHERE ROUND(metric, 2) = 0.10

Para valores monetários, prefira tipos decimais (quando você controla o schema), mas em consultas do dia a dia, o importante é reconhecer quando a igualdade exata não é confiável.

Filtragem com valores ausentes: incluir, excluir ou separar?

Decisão de negócio: NULL significa “não aplicável” ou “não informado”?

Antes de “tratar nulos”, defina o significado. Dois exemplos comuns:

  • Não informado: o dado deveria existir, mas está faltando (ex.: email).
  • Não aplicável: faz sentido não existir (ex.: canceled_at para pedidos não cancelados).

Essa distinção muda o filtro. Em relatórios, muitas vezes é melhor separar nulos em uma categoria do que substituí-los silenciosamente.

Padrões práticos

  • Para excluir nulos: col IS NOT NULL
  • Para incluir nulos junto com um valor: (col = 'X' OR col IS NULL)
  • Para rotular nulos em uma dimensão: COALESCE(col, 'unknown') (mais comum em SELECT e GROUP BY)

Exemplo: relatório de tickets por canal, separando nulos:

SELECT COALESCE(channel, 'unknown') AS channel_group, COUNT(*) AS total FROM tickets GROUP BY COALESCE(channel, 'unknown');

Armadilhas com NOT e De Morgan: negar condições compostas

NOT (A OR B) não é igual a (NOT A OR NOT B)

Negar condições compostas exige cuidado. As leis de De Morgan dizem:

  • NOT (A OR B) equivale a (NOT A) AND (NOT B)
  • NOT (A AND B) equivale a (NOT A) OR (NOT B)

Exemplo: você quer pedidos que não estão (cancelados ou devolvidos):

-- Correto SELECT * FROM orders WHERE NOT (status = 'canceled' OR status = 'returned');

Equivalente:

SELECT * FROM orders WHERE status <> 'canceled' AND status <> 'returned';

Agora, cuidado com NULL: se status for NULL, status <> 'canceled' vira UNKNOWN, e a linha não passa. Se você quer incluir status nulo, precisa explicitar:

SELECT * FROM orders WHERE (status IS NULL) OR (status <> 'canceled' AND status <> 'returned');

Passo a passo prático: construindo um filtro “à prova de nulos” para um relatório

Cenário

Você precisa listar pedidos “válidos para faturamento” com as seguintes regras:

  • Status deve ser 'paid' ou 'shipped'.
  • Não pode estar marcado como fraude (fraud_flag = 1).
  • Pedidos de teste devem ser excluídos (campo is_test pode ser 0, 1 ou NULL).
  • O período deve considerar created_at com hora.

Etapa 1: escrever a regra com agrupamentos

  • (status em {paid, shipped})
  • E (fraud_flag não é 1; se for nulo, tratar como não fraudado apenas se essa for a regra do negócio)
  • E (is_test não é 1; mas se for nulo, decidir se entra ou não)
  • E (created_at dentro do intervalo)

Etapa 2: decidir explicitamente o tratamento de nulos

Suponha as decisões:

  • fraud_flag nulo será tratado como 0 (não fraudado) porque o sistema antigo não preenchia.
  • is_test nulo será tratado como 0 (não teste) pelo mesmo motivo.

Essas decisões devem ser explícitas no SQL, para que quem leia entenda o impacto.

Etapa 3: aplicar intervalo semiaberto para datas

Período de janeiro de 2026:

  • Início: '2026-01-01'
  • Próximo início: '2026-02-01'

Etapa 4: montar a consulta final com parênteses e COALESCE

SELECT order_id, status, created_at, total FROM orders WHERE (status IN ('paid', 'shipped')) AND COALESCE(fraud_flag, 0) <> 1 AND COALESCE(is_test, 0) <> 1 AND created_at >= '2026-01-01' AND created_at < '2026-02-01';

Etapa 5: criar consultas de checagem para auditar o filtro

1) Quantos pedidos no período?

SELECT COUNT(*) AS total_period FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01';

2) Quantos seriam excluídos por fraude?

SELECT COUNT(*) AS fraud_excluded FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01' AND COALESCE(fraud_flag, 0) = 1;

3) Quantos seriam excluídos por teste?

SELECT COUNT(*) AS test_excluded FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01' AND COALESCE(is_test, 0) = 1;

4) Quantos têm status fora do esperado?

SELECT status, COUNT(*) FROM orders WHERE created_at >= '2026-01-01' AND created_at < '2026-02-01' GROUP BY status ORDER BY COUNT(*) DESC;

Essas checagens ajudam a detectar rapidamente se o filtro está “apertado demais” ou se há valores inesperados (incluindo NULL).

Checklist de armadilhas comuns em condições (para revisar antes de publicar um relatório)

  • Use IS NULL/IS NOT NULL em vez de = NULL/<> NULL.
  • Revise AND/OR e adicione parênteses sempre que houver mistura.
  • Evite NOT IN com subquery que pode retornar NULL; prefira NOT EXISTS ou filtre nulos.
  • Em datas com hora, prefira intervalos semiabertos (>= início e < próximo_início) em vez de BETWEEN com fim do período.
  • Normalize strings no filtro quando houver risco de espaços e variações (TRIM, UPPER), mas esteja ciente do impacto em performance.
  • Se usar NOT em condições compostas, aplique De Morgan corretamente e pense no efeito de NULL.
  • Se a regra depende de como tratar nulos, deixe isso explícito com COALESCE ou condições adicionais.
  • Faça checagens por partes (contagens por grupo) para validar o efeito de cada condição.

Agora responda o exercício sobre o conteúdo:

Ao filtrar pedidos por um mês usando uma coluna created_at com data e hora, qual abordagem tende a ser mais confiável para evitar exclusões no fim do período?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Em colunas com hora, BETWEEN pode interpretar o fim como meia-noite e excluir registros do último dia. O intervalo semiaberto (>= início e < próximo_início) evita o problema de borda e é mais previsível.

Próximo capitúlo

Junções na prática com INNER JOIN e LEFT JOIN usando dados reais fictícios

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.