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...Baixar o aplicativo
IS NULLIS 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): retornaNULLsex = y, senão retornax.
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 NULLComparaçõ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 é
DATEouTIMESTAMP. - Defina o período como intervalo semiaberto (
>= inícioe< 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.1Use tolerância (quando fizer sentido):
WHERE metric BETWEEN 0.0999 AND 0.1001Ou arredonde para uma casa definida (ciente do impacto):
WHERE ROUND(metric, 2) = 0.10Para 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_atpara 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 emSELECTeGROUP 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_testpode ser 0, 1 ouNULL). - O período deve considerar
created_atcom 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_flagnulo será tratado como 0 (não fraudado) porque o sistema antigo não preenchia.is_testnulo 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 NULLem vez de= NULL/<> NULL. - Revise
AND/ORe adicione parênteses sempre que houver mistura. - Evite
NOT INcom subquery que pode retornarNULL; prefiraNOT EXISTSou filtre nulos. - Em datas com hora, prefira intervalos semiabertos (
>= inícioe< próximo_início) em vez deBETWEENcom 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
NOTem condições compostas, aplique De Morgan corretamente e pense no efeito deNULL. - Se a regra depende de como tratar nulos, deixe isso explícito com
COALESCEou condições adicionais. - Faça checagens por partes (contagens por grupo) para validar o efeito de cada condição.