SQL para análise de dados · Guias · 30 de outubro de 2026 · 15 min de leitura

NULL no SQL: IS NULL, COALESCE, NULL em JOIN, COUNT e comparações

Um assento vazio numa fileira de poltronas ocupadas de avião

NULL significa “não há valor aqui”: desconhecido, ausente ou não aplicável. Não é zero, não é texto vazio e não é false. A maioria das surpresas com NULL vem de uma regra: qualquer comparação com NULL dá NULL (desconhecido), e não verdadeiro ou falso, e o WHERE só mantém as linhas em que a condição é verdadeira. Por isso x = NULL não encontra nada, NOT IN com um NULL na lista não encontra nada, e um NOT na frente de uma condição não leva as linhas NULL para o outro lado.

O kit prático é curto:

Os exemplos rodam no banco de dados de uma companhia aérea usado no curso de SQL da Nomto: 33.121 voos entre julho e setembro de 2026, 53.000 membros do programa de fidelidade, 1,5 milhão de eventos de app. Os textos dos dados estão em inglês. O SQL segue a sintaxe do PostgreSQL, e cada resultado saiu da execução real da consulta.

De onde vêm os NULLs

Tabelas reais têm NULLs por motivos diferentes, e o motivo define como tratá-los. Neste banco:

Contar os valores não nulos por status mostra bem o primeiro caso:

SELECT status,
       COUNT(*) AS flights,
       COUNT(actual_departure) AS dep,
       COUNT(actual_arrival) AS arr
FROM flights
GROUP BY status
ORDER BY flights DESC;
status flights dep arr
Arrived 16707 16707 16707
Scheduled 15383 0 0
On Time 518 0 0
Cancelled 414 8 8
Departed 58 58 0
Delayed 41 0 0

Os voos que já decolaram têm horário de partida, mas ainda não têm chegada. Os programados não têm nenhum dos dois. E oito voos cancelados têm horários, uma peculiaridade dos dados que você só enxerga contando NULLs.

Comparações: por que = NULL não encontra nada

SELECT NULL = NULL AS eq,
       NULL <> 1 AS ne,
       NULL IS NULL AS isnull,
       1 + NULL AS plus,
       'a' || NULL AS concat_op,
       concat('a', NULL) AS concat_fn;
eq ne isnull plus concat_op concat_fn
NULL NULL true NULL NULL a

Nem NULL = NULL é verdadeiro: não se sabe se dois valores desconhecidos são iguais. A aritmética e o operador || também propagam o NULL; o concat() é a exceção e trata NULL como texto vazio.

Por isso esta consulta devolve zero linhas, sem nenhum erro:

SELECT COUNT(*) FROM flights WHERE actual_departure = NULL;
count_star()
0

Enquanto esta encontra os 16.348 voos que ainda não decolaram:

SELECT COUNT(*) FROM flights WHERE actual_departure IS NULL;
count_star()
16348

Lógica de três valores: o NOT não pega os NULLs

Uma condição no SQL pode ser verdadeira, falsa ou desconhecida. O WHERE só fica com as verdadeiras. NOT desconhecido continua desconhecido, então as linhas NULL caem fora dos dois lados:

SELECT COUNT(*) FILTER (WHERE birth_year < 1980) AS before_1980,
       COUNT(*) FILTER (WHERE NOT (birth_year < 1980)) AS from_1980,
       COUNT(*) FILTER (WHERE birth_year IS NULL) AS unknown,
       COUNT(*) AS total
FROM loyalty_members;
before_1980 from_1980 unknown total
17334 29347 6319 53000

Uma condição e a negação dela somam 46.681, e não 53.000. Se você divide uma tabela em “atende” e “todo o resto” com WHERE cond e WHERE NOT cond, os NULLs não ficam em nenhuma das metades. Acrescente OR col IS NULL do lado a que eles pertencem.

O mesmo acontece com <>. WHERE status <> 'Arrived' pularia em silêncio as linhas com status NULL (esta tabela não tem nenhuma, mas muitas têm).

IS DISTINCT FROM: uma comparação segura com NULL

Quando você quer que o NULL seja tratado como um valor comum, de modo que dois NULLs contem como iguais, use IS DISTINCT FROM e IS NOT DISTINCT FROM:

SELECT NULL IS DISTINCT FROM NULL AS a,
       1 IS DISTINCT FROM NULL AS b,
       NULL IS NOT DISTINCT FROM NULL AS c;
a b c
false true true

Esses operadores sempre devolvem verdadeiro ou falso, nunca NULL. São úteis para verificar “este valor mudou?” entre dois retratos dos dados, quando uma mudança de NULL para algum valor precisa contar como mudança. O PostgreSQL e o DuckDB os suportam; o MySQL usa o operador <=> no lugar.

NULL nas agregações

Todas as funções de agregação, exceto COUNT(*), ignoram NULLs. Com três valores, um deles NULL:

SELECT AVG(x) AS avg_x, SUM(x) AS sum_x, COUNT(x) AS cnt_x, COUNT(*) AS cnt
FROM (VALUES (1), (NULL), (3)) t(x);
avg_x sum_x cnt_x cnt
2 4 2 3

A média é 4 / 2 = 2, e não 4 / 3. COUNT(x) conta valores não nulos, COUNT(*) conta linhas. Nos dados reais:

SELECT COUNT(*) AS events,
       COUNT(member_id) AS with_member,
       COUNT(*) - COUNT(member_id) AS anonymous
FROM app_events;
events with_member anonymous
1494723 582098 912625

A maioria dos eventos vem de visitantes que não fizeram login. COUNT(DISTINCT member_id) também ignora NULLs, então devolve 33.624 membros reais, e os visitantes anônimos não aparecem como um “membro” a mais.

Ignorar NULLs costuma ser o que você quer, e trocá-los por zero costuma ser errado. Compare a idade média dos membros com e sem COALESCE:

SELECT ROUND(AVG(2026 - birth_year), 1) AS avg_age_known,
       ROUND(AVG(COALESCE(2026 - birth_year, 0)), 1) AS avg_age_wrong
FROM loyalty_members;
avg_age_known avg_age_wrong
43.2 38

A segunda coluna trata 6.319 membros de idade desconhecida como recém-nascidos. Troque NULL por zero só quando NULL significa mesmo zero, como em “nenhuma venda neste dia”.

SUM sem nenhuma linha é NULL, não 0

SELECT SUM(amount) AS revenue,
       COALESCE(SUM(amount), 0) AS revenue_zero,
       COUNT(*) AS segments
FROM ticket_flights
WHERE amount > 100000;
revenue revenue_zero segments
NULL 0.00 0

Nenhum trecho custa tanto, então não há nada para somar. O COUNT devolve 0 quando não há linhas, mas SUM, AVG, MIN e MAX devolvem NULL. Envolva-os em COALESCE(..., 0) quando o resultado alimenta um relatório ou um cálculo, para que “sem receita” apareça como 0 e não como uma célula vazia.

COALESCE: o primeiro valor não nulo

COALESCE(a, b, c) devolve o primeiro argumento que não é NULL. Um uso típico é o valor alternativo: o horário real de partida, se ela aconteceu, senão o programado.

SELECT flight_no, scheduled_departure, actual_departure,
       COALESCE(actual_departure, scheduled_departure) AS best_known_departure
FROM flights
WHERE flight_no = 'PG0405'
  AND scheduled_departure BETWEEN '2026-08-10' AND '2026-08-14'
ORDER BY scheduled_departure;
flight_no scheduled_departure actual_departure best_known_departure
PG0405 2026-08-10 09:35:00 2026-08-10 09:37:00 2026-08-10 09:37:00
PG0405 2026-08-11 09:35:00 2026-08-11 09:38:00 2026-08-11 09:38:00
PG0405 2026-08-12 09:35:00 NULL 2026-08-12 09:35:00
PG0405 2026-08-13 09:35:00 NULL 2026-08-13 09:35:00

Todos os argumentos precisam ter tipos compatíveis. COALESCE(birth_year, 'unknown') mistura inteiro com texto: o PostgreSQL recusa, e o DuckDB falha com “Could not convert string ‘unknown’ to INT32” assim que encontra uma linha NULL. Faça o cast antes:

SELECT COALESCE(CAST(birth_year AS VARCHAR), 'unknown') AS birth_year,
       COUNT(*) AS members
FROM loyalty_members
WHERE birth_year IS NULL OR birth_year > 2007
GROUP BY 1
ORDER BY 1;
birth_year members
2008 574
unknown 6319

NULLIF: transformar um valor em NULL

NULLIF(a, b) devolve NULL se a = b, senão devolve a. A função mais conhecida dele é proteger uma divisão: dividir por NULL dá NULL em vez de erro.

SELECT aircraft_code,
       COUNT(*) AS seats,
       COUNT(*) FILTER (WHERE fare_conditions = 'Business') AS business,
       ROUND(COUNT(*) * 1.0
             / NULLIF(COUNT(*) FILTER (WHERE fare_conditions = 'Business'), 0), 1) AS seats_per_business
FROM seats
GROUP BY aircraft_code
ORDER BY seats DESC;
aircraft_code seats business seats_per_business
773 402 30 13.4
763 222 30 7.4
321 170 28 6.1
320 140 20 7
733 130 12 10.8
319 116 20 5.8
SU9 97 12 8.1
CR2 50 0 NULL
CN1 12 0 NULL

Sem NULLIF, o PostgreSQL interrompe a consulta inteira com “division by zero” por causa de dois aviões pequenos. (O DuckDB não dá erro aqui: sem NULLIF, ele mostra Infinity para CR2 e CN1, o que depois contamina qualquer média ou total calculado em cima. Ou seja, uma consulta que roda no DuckDB ainda pode falhar no PostgreSQL, e dar números estranhos nos dois. Escreva o NULLIF.)

O segundo uso: limpar valores falsos que deveriam ser NULL. Alguns sistemas gravam um valor de preenchimento, como 1900 ou 0, quando o dado é desconhecido. Este conjunto de dados tem 148 membros nascidos em “1900”:

SELECT COUNT(*) FILTER (WHERE birth_year = 1900) AS placeholder_1900,
       ROUND(AVG(2026 - birth_year), 1) AS avg_age_raw,
       ROUND(AVG(2026 - NULLIF(birth_year, 1900)), 1) AS avg_age_clean
FROM loyalty_members;
placeholder_1900 avg_age_raw avg_age_clean
148 43.2 42.9

Um passageiro frequente de 126 anos não é um cliente real. O NULLIF transforma o valor de preenchimento em NULL, e o AVG então o ignora como qualquer outro valor ausente.

NULL no GROUP BY e no ORDER BY

O GROUP BY é um dos lugares em que os NULLs são tratados como iguais: todos os NULLs formam um único grupo.

SELECT birth_year - birth_year % 10 AS decade, COUNT(*) AS members
FROM loyalty_members
GROUP BY decade
ORDER BY decade NULLS FIRST;
decade members
NULL 6319
1900 148
1940 47
1950 626
1960 4275
1970 12238
1980 16346
1990 9881
2000 3120

(birth_year - birth_year % 10 arredonda para baixo até a década em qualquer banco. O mais óbvio, birth_year / 10 * 10, funciona no PostgreSQL, onde a divisão inteira descarta o resto, mas não no DuckDB, onde o / devolve uma fração e você receberia 1946 de volta.)

Na ordenação, o PostgreSQL e o DuckDB discordam. O PostgreSQL trata NULL como maior que qualquer valor: os NULLs vêm por último em ordem crescente e primeiro em ordem decrescente. O DuckDB coloca os NULLs por último nas duas direções, por padrão. Então esta consulta:

SELECT member_id, birth_year
FROM loyalty_members
WHERE member_id <= 12
ORDER BY birth_year DESC;
member_id birth_year
7 2000
6 1987
2 1983
3 1977
5 1977
9 1973
10 1972
11 1969
1 1967
12 1967
4 NULL
8 NULL

mostra os membros 4 e 8 no fim no DuckDB, como acima, mas no topo no PostgreSQL. Um “top 10 membros mais jovens” com LIMIT 10 devolveria dois desconhecidos no PostgreSQL. Escreva NULLS FIRST ou NULLS LAST explicitamente sempre que a coluna puder ser NULL; os dois bancos aceitam.

NULL em joins

Chaves NULL nunca se correspondem

Uma condição de join é uma comparação, então um NULL de qualquer lado nunca é igual a nada:

SELECT COUNT(*) AS events, COUNT(m.member_id) AS joined
FROM app_events e
LEFT JOIN loyalty_members m ON m.member_id = e.member_id;
events joined
1494723 582098

Os 912.625 eventos anônimos não encontram nenhum membro. Com um inner join, eles sumiriam do resultado, o que pode estar ok para “eventos por membro” e muito errado para “eventos por dia”. Se você precisa mesmo que duas chaves NULL se correspondam, faça o join com ON a.key IS NOT DISTINCT FROM b.key, mas pense duas vezes: dois visitantes anônimos não são a mesma pessoa.

Um LEFT JOIN cria NULLs

Quando uma linha da esquerda não tem correspondência, as colunas da direita são preenchidas com NULL. É assim que você encontra “pais sem filhos”, e também é por isso que COUNT(*) depois de um LEFT JOIN pode enganar:

SELECT a.aircraft_code, a.model,
       COUNT(*) AS count_star,
       COUNT(f.flight_id) AS flights
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code
GROUP BY a.aircraft_code, a.model
ORDER BY flights
LIMIT 3;
aircraft_code model count_star flights
320 Airbus A320-200 1 0
773 Boeing 777-300 610 610
763 Boeing 767-300 1221 1221

O A320 não tem voos, mas o LEFT JOIN ainda produz uma linha para ele, com NULLs do lado dos voos. O COUNT(*) conta essa linha e diz 1. O COUNT(f.flight_id) conta valores não nulos e diz, corretamente, 0. Depois de um LEFT JOIN, conte uma coluna da tabela da direita.

A armadilha do WHERE

Filtrar a tabela da direita no WHERE joga fora justamente as linhas NULL que o LEFT JOIN criou:

SELECT a.aircraft_code, COUNT(f.flight_id) AS cancelled
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code
WHERE f.status = 'Cancelled'
GROUP BY a.aircraft_code
ORDER BY cancelled;
aircraft_code cancelled
773 1
321 3
763 6
733 15
319 24
SU9 69
CN1 115
CR2 181

Oito modelos, não nove. Na linha do A320, f.status é NULL, NULL = 'Cancelled' é desconhecido, e o WHERE a descarta. Leve a condição para o ON:

SELECT a.aircraft_code, COUNT(f.flight_id) AS cancelled
FROM aircrafts a
LEFT JOIN flights f ON f.aircraft_code = a.aircraft_code AND f.status = 'Cancelled'
GROUP BY a.aircraft_code
ORDER BY cancelled;
aircraft_code cancelled
320 0
773 1
321 3
763 6
733 15
319 24
SU9 69
CN1 115
CR2 181

Agora o filtro decide quais voos entram no join, e todos os modelos ficam. O guia de JOIN no SQL e o guia de date spine (em inglês) mostram a mesma armadilha em outros contextos.

A armadilha do NOT IN

x NOT IN (a, b, c) significa x <> a AND x <> b AND x <> c. Se algum elemento for NULL, uma dessas comparações é desconhecida, e a condição inteira nunca pode ser verdadeira:

SELECT 1 IN (1, NULL) AS in_hit,
       1 IN (2, NULL) AS in_miss,
       1 NOT IN (2, NULL) AS not_in_miss,
       1 NOT IN (2, 3) AS not_in_clean;
in_hit in_miss not_in_miss not_in_clean
true NULL NULL true

Com uma subconsulta, o NULL fica invisível, o que é pior. Quais membros do programa de fidelidade não têm eventos no app?

SELECT COUNT(*) AS members_without_events
FROM loyalty_members
WHERE member_id NOT IN (SELECT member_id FROM app_events);
members_without_events
0

A subconsulta devolve centenas de milhares de NULLs (os eventos anônimos), então a resposta é zero. O NOT EXISTS compara linha por linha e não é afetado:

SELECT COUNT(*) AS members_without_events
FROM loyalty_members m
WHERE NOT EXISTS (
  SELECT 1 FROM app_events e WHERE e.member_id = m.member_id
);
members_without_events
19376

Use NOT EXISTS (ou um anti join LEFT JOIN ... WHERE right.key IS NULL) para “linhas sem correspondência”. Se mantiver o NOT IN, acrescente WHERE member_id IS NOT NULL dentro da subconsulta. Mais padrões de subconsulta estão no guia de subconsulta SQL.

NULL no CASE

Um ramo de CASE cuja condição dá NULL é pulado, então as linhas NULL caem no ELSE. CASE WHEN birth_year < 1980 THEN 'older' ELSE 'younger' END coloca todo membro com ano de nascimento desconhecido entre os “younger”. Coloque WHEN birth_year IS NULL THEN 'unknown' primeiro. O guia de CASE WHEN no SQL mostra isso com contagens reais.

Checklist

Perguntas frequentes

Por que WHERE coluna = NULL não devolve nenhuma linha?

Porque comparar qualquer coisa com NULL dá NULL (desconhecido), e não verdadeiro, e o WHERE só mantém as linhas em que a condição é verdadeira. Use WHERE coluna IS NULL.

Qual a diferença entre COUNT(*) e COUNT(coluna)?

COUNT(*) conta linhas. COUNT(coluna) conta as linhas em que essa coluna não é NULL. Em app_events, a diferença é de 912.625: os eventos sem membro.

Qual a diferença entre COALESCE e NULLIF?

São opostos. COALESCE(a, b) troca um NULL por um valor. NULLIF(a, b) troca um valor por NULL quando a é igual a b, normalmente NULLIF(x, 0) antes de uma divisão.

NULL é igual a texto vazio?

Não. No PostgreSQL e no DuckDB, '' é um valor real de comprimento zero, e '' IS NULL é falso. O Oracle é a exceção: ele trata texto vazio como NULL.

Como os NULLs são ordenados no SQL?

Depende do banco. O PostgreSQL ordena os NULLs por último em ordem crescente e primeiro em ordem decrescente; o DuckDB os coloca por último nos dois casos. Escreva NULLS FIRST ou NULLS LAST para ter o mesmo resultado em todo banco que aceita essa sintaxe.

Outros idiomas: English, Español, Tiếng Việt, 简体中文