Pular para o conteúdo
Health Check gratuito
Fundamentos

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

7 min de leitura testado em Oracle 19c
NVL avalia os dois lados, comparando NVL, NVL2 e COALESCE

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.

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