Pular para o conteúdo
Health Check gratuito
Fundamentos

NVL, NVL2 e COALESCE no Oracle: qual usar e por quê

NVL, NVL2 e COALESCE no Oracle: qual usar em cada caso, por que o NVL avalia sempre os dois lados e o que o tratamento de nulo faz com o índice.

7 min de leitura testado em Oracle 19c
Uma esteira de blocos de dados azuis onde os blocos vazios são preenchidos por um cabeçote sob luz azul

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

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.

Continue lendo

Quer saber como o seu banco está de verdade?

O Health Check é gratuito e devolve um diagnóstico do seu ambiente Oracle ou SQL Server, com a mesma régua que eu uso nos artigos.

Fazer o Health Check