NVL, NVL2 e COALESCE no Oracle: qual usar e por quê
Semana passada revisei um relatório de faturamento de uma rede de lojas onde o campo de desconto aparecia vazio em umas trezentas linhas. O dev tinha colocado um `nvl` na consulta para

Semana passada revisei um relatório de faturamento de uma rede de lojas onde o campo de desconto aparecia vazio em umas trezentas linhas. O dev tinha colocado um nvl na consulta para resolver, o valor apareceu, todo mundo ficou satisfeito, e três dias depois o mesmo relatório começou a demorar quarenta minutos. O NVL não era o culpado direto, mas o jeito como ele foi usado, sim. Esse tipo de coisa acontece o tempo todo com as funções de tratamento de nulo do Oracle, porque elas parecem intercambiáveis e não são.
o que o nvl faz, na prática
NVL recebe dois argumentos. Se o primeiro for nulo, devolve o segundo. Se não for, devolve o primeiro.
SELECT NVL(NULL, 'sem valor') AS resultado FROM dual;
-- resultado: sem valor
SELECT NVL('preenchido', 'sem valor') AS resultado FROM dual;
-- resultado: preenchido
Num cenário de verdade, imagine uma tabela de pedidos onde o desconto só é gravado quando existe:
CREATE TABLE pedidos (
id_pedido NUMBER PRIMARY KEY,
id_cliente NUMBER,
valor_bruto NUMBER(12,2),
desconto NUMBER(12,2),
status VARCHAR2(1)
);
INSERT INTO pedidos VALUES (1, 100, 1500.00, 150.00, 'A');
INSERT INTO pedidos VALUES (2, 101, 890.00, NULL, 'A');
INSERT INTO pedidos VALUES (3, 102, 2300.00, NULL, NULL);
COMMIT;
SELECT id_pedido,
valor_bruto - NVL(desconto, 0) AS valor_liquido
FROM pedidos;
Sem o NVL, o pedido 2 retornaria nulo no valor líquido, porque qualquer aritmética com nulo dá nulo. Esse é o uso mais comum e o mais defensável.
nvl2, quando o resultado muda dos dois lados
NVL2 recebe três argumentos e inverte a lógica de quem você está olhando. Ele testa o primeiro argumento e devolve o segundo se houver valor, ou o terceiro se for nulo.
SELECT id_pedido,
NVL2(desconto, 'COM DESCONTO', 'SEM DESCONTO') AS marcador,
NVL2(desconto, valor_bruto - desconto, valor_bruto) AS valor_liquido
FROM pedidos;
Repare que o segundo argumento pode ser uma expressão que usa a própria coluna testada. Uso NVL2 quando preciso de comportamentos diferentes nos dois caminhos, o que um NVL sozinho não resolve sem virar um CASE. Fora disso, prefiro CASE mesmo, porque lê melhor para quem pega o código depois.
coalesce e a lista de fallbacks
COALESCE aceita quantos argumentos você quiser e devolve o primeiro que não for nulo. Isso muda o desenho da solução quando existe uma cadeia de origens possíveis para o mesmo dado. Já mexi num sistema hospitalar onde o telefone de contato do paciente podia estar no cadastro, no convênio ou no registro do acompanhante, nessa ordem de preferência.
SELECT p.id_paciente,
COALESCE(p.telefone_celular,
p.telefone_fixo,
c.telefone_convenio,
'NAO INFORMADO') AS contato
FROM pacientes p
LEFT JOIN convenios c ON c.id_convenio = p.id_convenio;
Fazer isso com NVL exigiria aninhar três chamadas. Funciona, fica ilegível.
a diferença que mais pega gente: nvl avalia todos os argumentos
Aqui está o ponto que quase ninguém conhece e que já vi quebrar consulta em produção. NVL avalia os dois argumentos antes de decidir qual devolver. COALESCE faz short-circuit, ou seja, para no primeiro argumento não nulo e nem chega a executar os seguintes.
O teste é de uma linha:
SELECT COALESCE(1, 1/0) FROM dual;
-- retorna 1
SELECT NVL(1, 1/0) FROM dual;
-- ORA-01476: divisor is equal to zero
Com literais isso parece curiosidade acadêmica. Troque 1/0 por uma subquery correlacionada que varre uma tabela de dez milhões de linhas, ou por uma função PL/SQL que faz round-trip num serviço externo, e a curiosidade vira incidente. Um NVL vai pagar esse custo em toda linha do resultado, mesmo quando o primeiro argumento já tinha valor em 99% dos casos.
Dá para medir com um contador:
CREATE OR REPLACE PACKAGE pk_teste AS
g_chamadas PLS_INTEGER := 0;
FUNCTION valor_caro RETURN NUMBER;
END pk_teste;
/
CREATE OR REPLACE PACKAGE BODY pk_teste AS
FUNCTION valor_caro RETURN NUMBER IS
BEGIN
g_chamadas := g_chamadas + 1;
RETURN 0;
END;
END pk_teste;
/
BEGIN pk_teste.g_chamadas := 0; END;
/
SELECT NVL(valor_bruto, pk_teste.valor_caro) FROM pedidos;
SELECT pk_teste.g_chamadas FROM dual; -- conta uma chamada por linha
BEGIN pk_teste.g_chamadas := 0; END;
/
SELECT COALESCE(valor_bruto, pk_teste.valor_caro) FROM pedidos;
SELECT pk_teste.g_chamadas FROM dual; -- zero, nenhuma linha tem valor_bruto nulo
O otimizador às vezes reescreve NVL internamente, então o comportamento pode variar por versão e por plano. Não conte com isso. Se o segundo argumento é caro, use COALESCE e durma tranquilo.
conversão implícita de tipo
NVL converte o segundo argumento para o tipo do primeiro. Silenciosamente, quando dá, e com erro quando não dá.
SELECT NVL(desconto, 'sem valor') FROM pedidos;
-- ORA-01722: invalid number
O Oracle tentou transformar a string em número porque a coluna é NUMBER. O problema fica pior no caminho inverso, onde a conversão dá certo mas o resultado surpreende:
SELECT NVL(status, 0) FROM pedidos;
-- devolve '0' como VARCHAR2, não o número zero
Se alguém depois ordenar ou comparar esse resultado esperando número, a ordenação sai alfabética e ninguém entende por quê. COALESCE é mais rigoroso: ele exige que todos os argumentos sejam de tipos compatíveis e falha na cara com ORA-00932 quando não são.
SELECT COALESCE(desconto, 'sem valor') FROM pedidos;
-- ORA-00932: inconsistent datatypes: expected NUMBER got CHAR
Prefiro o erro explícito. Erro que aparece em desenvolvimento custa muito menos que dado errado que aparece em relatório fechado.
coalesce é ANSI, nvl não
COALESCE está no padrão SQL e funciona em SQL Server, PostgreSQL, MySQL, DB2. NVL e NVL2 são do Oracle. Já participei de dois projetos de migração de Oracle para PostgreSQL e, nos dois, a quantidade de NVL espalhado pelas views e procedures foi um item de esforço real no cronograma. Se existe qualquer chance do seu código sair do Oracle algum dia, escrever COALESCE desde o começo é barato agora e caro depois.
o impacto no índice, que aparece em produção
Esse é o que derruba relatório. Quando você aplica NVL numa coluna dentro do WHERE, a coluna deixa de estar disponível para o índice e o otimizador cai em full table scan.
CREATE INDEX ix_pedidos_status ON pedidos (status);
-- não usa o índice
SELECT * FROM pedidos WHERE NVL(status, 'A') = 'A';
-- usa o índice para a parte de igualdade
SELECT * FROM pedidos WHERE status = 'A' OR status IS NULL;
Vale lembrar que índice B-tree de coluna única no Oracle não armazena entradas totalmente nulas, então o IS NULL sozinho também não tem índice para usar. Se esse predicado é frequente, as saídas são criar um índice baseado em função ou um índice composto que force o registro dos nulos:
CREATE INDEX ix_pedidos_status_nvl ON pedidos (NVL(status, 'A'));
-- ou
CREATE INDEX ix_pedidos_status_id ON pedidos (status, id_pedido);
Com o índice baseado em função, a consulta original passa a usar índice, desde que a expressão no WHERE seja idêntica à do índice. COALESCE no WHERE tem exatamente o mesmo efeito bloqueador, então não troque um pelo outro esperando ganho de performance nesse ponto.
o que eu recomendo
Minha regra de bolso é simples. Use COALESCE como padrão em código novo, porque ele é portável, faz short-circuit e falha de forma clara em incompatibilidade de tipo. Use NVL quando for um tratamento trivial de nulo com literal barato, o código já está cheio de NVL e a consistência importa mais que a pureza. Use NVL2 nos casos em que o valor de saída muda nos dois caminhos e um CASE ficaria mais verboso sem ganhar clareza.
E em qualquer um dos três, mantenha a função fora do WHERE sempre que puder. O tratamento de nulo pertence ao SELECT, não ao filtro.
Se esse tipo de detalhe te pegou de surpresa, é sinal de que vale fechar as lacunas de base antes que elas apareçam num plantão. É exatamente isso que trabalho no curso Oracle Fundamentals, onde cada conceito vem com o teste que você pode rodar no seu próprio ambiente.
Marcio Mandarino
DBA Oracle e SQL Server há mais de 20 anos. Atendo plantão, recuperação de incidente e gestão contínua de bancos de dados. Este artigo saiu de um laboratório que rodou antes de virar texto.

