NULL não é zero, não é texto vazio e não é igual a si mesmo. Em SQL ele representa ausência de valor, e qualquer comparação com ele devolve UNKNOWN — nem verdadeiro, nem falso. Essa regra é a origem da maioria dos relatórios com total errado, COUNT diferente do esperado e listas que voltam vazias sem motivo aparente.
O que é NULL e por que = NULL nunca funciona?
SQL usa lógica de três valores: TRUE, FALSE e UNKNOWN. NULL significa "não sei o valor", então desconto = NULL é UNKNOWN, não TRUE. Como o WHERE só deixa passar linhas cujo resultado é TRUE, a query não retorna nada.
-- Errado: nunca retorna linha, em nenhum banco
SELECT id, nome FROM clientes WHERE email = NULL;
-- Correto: IS NULL testa a ausência do valor
SELECT id, nome FROM clientes WHERE email IS NULL;
-- O complemento também é IS NOT NULL (<> NULL não serve)
SELECT id, nome FROM clientes WHERE email IS NOT NULL;
O erro é silencioso: não há exceção, só uma lista vazia. Em produção ele costuma aparecer como "o filtro não funciona" e consome horas porque a query parece correta.
Por que COUNT(*) e COUNT(coluna) retornam números diferentes?
Porque COUNT(*) conta linhas e COUNT(coluna) conta valores não nulos daquela coluna. As demais funções agregadas (SUM, AVG, MIN, MAX) também ignoram NULL — e é aí que o total passa a divergir do que o usuário espera.
-- Massa de teste: 4 pedidos, um deles sem cliente
-- id | cliente_id | desconto
-- 1 | 10 | 5.00
-- 2 | 11 | NULL
-- 3 | NULL | 10.00
-- 4 | 12 | NULL
SELECT COUNT(*) AS linhas, -- 4
COUNT(cliente_id) AS com_cliente, -- 3 (ignora o NULL)
COUNT(desconto) AS com_desconto, -- 2 (ignora os dois NULL)
SUM(desconto) AS soma_desconto, -- 15.00
AVG(desconto) AS media_desconto -- 7.50, não 3.75
FROM pedidos;
Repare no AVG: ele divide a soma por 2 (quantidade de valores não nulos), não por 4. Se o relatório promete "desconto médio por pedido", o número está inflado — e ninguém percebe até alguém conferir na calculadora.
| Função | Ignora NULL? |
Comportamento |
|---|---|---|
COUNT(*) |
Não | Conta linhas, inclusive as totalmente nulas |
COUNT(col) |
Sim | Conta só valores preenchidos |
SUM(col) |
Sim | Soma os não nulos |
AVG(col) |
Sim | Divide pelos não nulos, não pelo total de linhas |
MIN / MAX |
Sim | Ignoram NULL no cálculo |
COALESCE |
— | Devolve o primeiro argumento não nulo |
Por que SUM pode devolver NULL em vez de 0?
Quando todas as linhas do grupo são NULL, a soma é NULL, não 0. Isso quebra a conta em cascata: NULL + 100 continua NULL, e o total do relatório vira vazio em vez de 100.
-- Se todos os descontos do período forem NULL, o resultado é NULL
SELECT SUM(desconto) FROM pedidos
WHERE data_pedido BETWEEN '2026-01-01' AND '2026-01-31';
-- Correção: trate o NULL na origem do cálculo
SELECT COALESCE(SUM(desconto), 0) AS total_desconto
FROM pedidos
WHERE data_pedido BETWEEN '2026-01-01' AND '2026-01-31';
-- Se o objetivo é "considerar desconto ausente como zero", faça na coluna
SELECT SUM(COALESCE(desconto, 0)) AS total
FROM pedidos
WHERE data_pedido BETWEEN '2026-01-01' AND '2026-01-31';
A diferença entre os dois COALESCE importa: o primeiro devolve 0 só quando não há linha nenhuma com valor; o segundo trata cada NULL como zero. Para "somar descontos concedidos", o segundo é o correto.
Por que NOT IN com NULL volta vazio?
Este é o bug mais caro da lista. Se a subquery do NOT IN retornar um único NULL, nenhuma linha passa no filtro — porque x <> NULL é UNKNOWN para todo x.
-- Errado: se existir um pedido com cliente_id NULL, retorna zero linhas
SELECT id, nome FROM clientes
WHERE id NOT IN (SELECT cliente_id FROM pedidos);
-- Correção 1: NOT EXISTS, que não sofre com NULL
SELECT c.id, c.nome FROM clientes c
WHERE NOT EXISTS (
SELECT 1 FROM pedidos p WHERE p.cliente_id = c.id
);
-- Correção 2: se NOT IN for obrigatório, elimine o NULL na subquery
SELECT id, nome FROM clientes
WHERE id NOT IN (
SELECT cliente_id FROM pedidos WHERE cliente_id IS NOT NULL
);
A query "errada" está no ar há anos e funcionou até o dia em que a coluna passou a aceitar NULL. Esse é o tipo de regressão que aparece sem deploy de código — só de mudar o schema ou entrar um registro incompleto.
É nesse filtro que a lógica de três valores cobra o preço. O Sqlify transforma a descrição em português na query pronta, com cada cláusula explicada — inclusive o tratamento de nulos.
Como NULL se comporta em GROUP BY e ORDER BY?
GROUP BY trata todos os NULL como um único grupo — não gera erro, mas cria a linha "(vazio)" no relatório. Já o ORDER BY varia entre bancos, e é onde a diferença entre Firebird e MySQL aparece na prática.
-- Todos os NULL ficam no mesmo grupo, contados juntos
SELECT categoria_id, COUNT(*) AS qtd
FROM produtos
GROUP BY categoria_id
ORDER BY categoria_id;
-- Firebird: NULL é maior que qualquer valor
-- ASC -> NULL no fim | DESC -> NULL no começo
-- Desde o 1.5 é possível forçar a posição:
SELECT nome, categoria_id FROM produtos
ORDER BY categoria_id ASC NULLS LAST;
| Banco | NULL em ORDER BY ... ASC |
Como inverter | Equivalente a COALESCE |
|---|---|---|---|
| Firebird | Por último | NULLS FIRST / LAST (1.5+) |
COALESCE(col, valor) |
| PostgreSQL | Por último | NULLS FIRST / LAST |
COALESCE(col, valor) |
| Oracle | Por último | NULLS FIRST / LAST |
NVL(col, valor) |
| MySQL | Primeiro | ORDER BY col IS NULL, col |
IFNULL(col, valor) |
| SQL Server | Primeiro | ORDER BY CASE WHEN col IS NULL THEN 1 ELSE 0 END |
ISNULL(col, valor) |
Se a ordenação precisa ser idêntica entre bancos, ordene explicitamente por col IS NULL, col — funciona em todos e não depende de extensão de dialeto.
Como concatenar texto quando há NULL?
Em Firebird e na maioria dos bancos, 'a' || NULL resulta em NULL, não em 'a'. Um único campo opcional nulo apaga o endereço inteiro na geração de relatório ou de etiqueta.
-- Errado: se complemento for NULL, o resultado é NULL
SELECT nome || ' - ' || endereco || ', ' || complemento AS endereco_completo
FROM clientes;
-- Correto: cada parte opcional protegida
SELECT nome || ' - ' || endereco
|| COALESCE(', ' || complemento, '') AS endereco_completo
FROM clientes;
Atenção ao detalhe: COALESCE(', ' || complemento, '') preserva a vírgula só quando existe complemento. Trocar por ', ' || COALESCE(complemento, '') produz a vírgula sobrando no fim.
Checklist antes de publicar a query
- Nunca usar
= NULL,<> NULLouIN (NULL) -
COUNT(*)só quando a pergunta é "quantas linhas" -
SUM/AVGcomCOALESCEquando o relatório promete número -
NOT INtrocado porNOT EXISTS(ou subquery semNULL) - Ordenação com
NULLtestada nos dois sentidos - Concatenação com coluna opcional protegida
-
IS NULLpresente nos filtros que o usuário chama de "vazio"
Perguntas frequentes
NULL é igual a zero em SQL?
Não. NULL é ausência de valor; 0 é um número. SUM sobre coluna vazia retorna NULL, enquanto SUM(COALESCE(col, 0)) retorna 0. Confundir os dois é o que faz o relatório exibir "sem dados" quando o total real é zero.
O que é lógica de três valores?
É o modelo em que toda expressão SQL pode resultar em TRUE, FALSE ou UNKNOWN. NULL produz UNKNOWN em qualquer comparação, e o WHERE filtra as linhas UNKNOWN. Por isso NOT (NULL = 1) também não retorna a linha.
Qual a diferença entre COALESCE e NULLIF?
COALESCE(a, b, c) devolve o primeiro argumento não nulo. NULLIF(a, b) devolve NULL quando a é igual a b — útil para transformar 0 em NULL e evitar divisão por zero: SUM(valor) / NULLIF(COUNT(*), 0).
Como NULL afeta a chave estrangeira?
Nenhum problema: NULL em coluna de FK é permitido (a menos que haja NOT NULL) e não viola a constraint. Já em coluna de índice único, cada banco trata de forma diferente — Firebird e PostgreSQL aceitam múltiplos NULL na mesma coluna única, e o SQL Server aceita apenas um.
Conclusão
NULL não quebra a query; quebra o resultado. Trate como ausência de valor, use IS NULL para testar, COALESCE para calcular e NOT EXISTS no lugar de NOT IN. O Sqlify transforma a descrição em português na query pronta, já com o tratamento de nulos explícito e cada cláusula explicada.
Leia também: