← Voltar ao blog

NULL em SQL: por que COUNT e SUM dão resultados errados

NULL não é zero nem string vazia: por que COUNT, SUM, comparação e NOT IN devolvem resultado errado em SQL e como reescrever a query correta.


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, <> NULL ou IN (NULL)
  • COUNT(*) só quando a pergunta é "quantas linhas"
  • SUM/AVG com COALESCE quando o relatório promete número
  • NOT IN trocado por NOT EXISTS (ou subquery sem NULL)
  • Ordenação com NULL testada nos dois sentidos
  • Concatenação com coluna opcional protegida
  • IS NULL presente 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: