Por que a soma de FLOAT não bate? O erro de arredondamento em ponto flutuante, explicado com gráficos interativos

Você confere as notas uma a uma e todos os valores estão certos… e mesmo assim, ao somar as vendas do mês, o total fica um centavo diferente do depósito bancário. Ou você guardou preço × alíquota de imposto numa coluna FLOAT, e agora o SUM se recusa a bater. A maioria desses mistérios de “cada linha está certa, mas o total está errado” não é bug do seu código: eles acontecem porque números de ponto flutuante (FLOAT) só conseguem armazenar a maioria dos valores de forma aproximada.

Neste artigo vamos ver por que os totais se desviam, com fórmulas e gráficos interativos que você pode manipular.

💡 Dica

As figuras 1, 2 e 3 são gráficos vivos, que você pode mexer aqui mesmo na página. Fique à vontade para brincar primeiro com as figuras e voltar ao texto depois.

Se o que você precisa é de orientação para escolher tipos de coluna, isso está em Como escolher o tipo numérico correto em SQL (o clássico erro de “dinheiro em FLOAT” e um guia de decisão FLOAT vs DECIMAL).

O que este artigo responde

Pergunta Onde
Por que um decimal tão banal quanto 0.1 causa problema? §1 / fig. 1
Por que um erro invisível numa linha aparece no total? fig. 2
FLOAT e DECIMAL divergem mesmo com as mesmas notas? fig. 3 / tabela 1
Como o famoso 0.1 + 0.2 ≠ 0.3 entra nessa história? §5
O que fazer na prática? §6 / FAQ

Para cobrir uma faixa enorme de números com uma quantidade fixa de bytes, FLOAT / DOUBLE armazenam a maioria das frações decimais como uma aproximação arredondada para o valor representável mais próximo. Esse erro não é defeito: é uma troca deliberada de exatidão por velocidade e alcance.

E é exatamente por isso que esses tipos combinam mal com números que precisam bater até o último centavo: preços, alíquotas, totais de fatura. Aí a resposta certa é DECIMAL (NUMERIC), que garante os dígitos decimais. Para grandezas que já carregam ruído de medição, como leituras de sensores ou estatísticas, o pequeno arredondamento do FLOAT em geral não faz diferença na prática.

Uma imagem mental útil: uma régua com marcações faltando. O decimal mais comum do mundo, 0.1, não cai em nenhuma marcação da régua binária; ele é puxado para a marcação mais próxima, e somas repetidas vão empilhando esses puxões. Essa é a anatomia do centavo que não bate no total do mês.

1. Por que o 0.1 decimal complica a vida do binário

No papel, 0.1 é um decimal finito e limpo. Mas dentro da máquina, o FLOAT representa números em binário (zeros e uns), seguindo o padrão IEEE 754. Converta 0.1 para binário e ele se repete para sempre.

\[ 0.1_{10} = 0.0001100110011\ldots_{2} \]

Com um número finito de bits, é preciso cortar o valor e arredondá-lo para a marcação mais próxima. Escrevendo essa operação como \(\mathrm{fl}(x)\), o que realmente fica armazenado não é \(x\), e sim \(\mathrm{fl}(x)\).

\[ \mathrm{armazenado}(x) = \mathrm{fl}(x) \neq x \quad (\text{para muitas frações decimais}) \]

Um único arredondamento costuma ser minúsculo. O problema aparece quando você soma a aproximação repetidas vezes.

2. As marcações que o FLOAT32 consegue de fato acertar (fig. 1)

A figura abaixo desenha, como pontos, os valores que a precisão simples do IEEE 754 (FLOAT32) consegue de fato representar em torno do alvo que você escolher. A linha tracejada é “o valor decimal que você quer”; o ponto cheio fl é “a marcação mais próxima que o FLOAT32 consegue guardar”. Sempre que a linha tracejada erra os pontos, esse erro é o erro de arredondamento.

Figura 1  As marcações que o FLOAT32 consegue de fato acertar (reta numérica e fl mais próximo)

A vista abre estreita (8 ULP para cada lado, ou seja, oito espaçamentos de marcação). Repare na distância entre a linha tracejada (o valor que você quer) e o ponto azul fl (o valor FLOAT32 mais próximo). Alargue no controle e as marcações se amontoam; estreite e o espaçamento individual volta a aparecer.

No mundo ideal, um total é cada valor somado como está. No FLOAT, cada soma arredonda o resultado de novo.

\[ s_0 = 0, \qquad s_{k+1} = \mathrm{fl}(s_k + x_k) \]

Cada \(\mathrm{fl}\) sozinho é pequeno, mas conforme \(k\) cresce, a distância entre \(s_n\) e o total ideal pode ficar visível. É assim que um erro que ninguém nota numa nota isolada aparece pela primeira vez no SUM do mês.

# A ideia (pseudocódigo) — x pode variar de nota para nota
s = 0
para cada valor x_k:
    s = fl(s + x_k)   # arredondado para a marcação mais próxima toda vez
# O ideal é x_1+…+x_n. s pode não bater com ele.

3. O erro acumulado conforme as notas se empilham (fig. 2)

O eixo horizontal é o número de somas \(n\); o vertical, a diferença entre o total acumulado em FLOAT32 e o total decimal ideal. Como numa loja de verdade, o valor \(x_k\) percorre um padrão cíclico em vez de ficar constante (o controle define o valor-base).

Para que lado cada arredondamento cai depende de onde o total acumulado está entre duas marcações (o espaçamento das marcações, o ULP, muda com a magnitude) e do próximo valor \(x_k\). Por isso o erro não está condenado a subir em linha reta: ele pode ir para o negativo, voltar, ou tremer. As ondulações largas aparecem quando o viés do arredondamento aponta um tempo na mesma direção; o serrilhado fino vem da direção do arredondamento virando conforme os valores mudam.

Vale sublinhar: esse balanço não é aleatoriedade. Some o mesmo valor ao mesmo total acumulado e o resultado será o mesmo, sempre. Os valores da figura seguem um ciclo fixo de multiplicadores; nenhum dado é jogado em tempo de execução. Parece errático, mas o mecanismo é totalmente determinístico.

Figura 2  O erro acumulado a cada soma (total acumulado FLOAT32 − ideal)

A lição: uma diferença invisível num lançamento fica visível quando os lançamentos se acumulam. Aumente \(n_{\max}\) e compare a ordem de grandeza do desvio final no painel.

4. Somando as mesmas notas em FLOAT e em DECIMAL (fig. 3)

A figura 2 desenhava o tamanho do desvio. Agora mudamos o ponto de vista e observamos como a mesma coluna de notas se divide em dois totais, um somado em FLOAT e outro em DECIMAL, com a fórmula, uma tabela e um gráfico avançando juntos. Os valores percorrem o mesmo padrão de multiplicadores da figura 2 (base nominal \(0.10\)).

O DECIMAL soma cada \(x_k\) exatamente, em decimal. O FLOAT32 intercala \(\mathrm{fl}\) a cada passo.

\[ S_{\mathrm{DECIMAL}}(n) = x_1 + x_2 + \cdots + x_n \]

\[ S_{\mathrm{FLOAT}}(n) = \mathrm{fl}\bigl(\cdots \mathrm{fl}(\mathrm{fl}(0 + \mathrm{fl}(x_1)) + \mathrm{fl}(x_2)) \cdots + \mathrm{fl}(x_n)\bigr) \]

Escrito em SQL, um exemplo mínimo fica assim. O mesmo SUM; só os tipos das colunas mudam.

-- Um exemplo mínimo em SQL:
-- os valores variam por linha e, ainda assim, o mesmo SUM pode divergir pelo tipo
SELECT SUM(amount_float)    AS sum_float,     -- FLOAT / REAL
       SUM(amount_decimal)  AS sum_decimal    -- p. ex. DECIMAL(10,2)
FROM sales;

Na tabela 1 abaixo, as primeiras linhas quase não mostram diferença, e o desvio só sai do escuro na linha final, depois que os lançamentos se empilharam (o lado DECIMAL é o total exato, calculado em escala inteira).

Tabela 1  Totais acumulados FLOAT32 vs DECIMAL com as mesmas notas (5 primeiras linhas e linha final)
Linha Nota (exibida) Acumulado FLOAT32 Acumulado DECIMAL Desvio (F−D)

A figura 3 mostra o mesmo processo da tabela 1, mas com o total DECIMAL como linha de base (desvio zero). O eixo vertical não é o total em si: é SUM do FLOAT menos SUM do DECIMAL. A linha tracejada laranja marca “desvio zero (bate com o DECIMAL)”; a curva azul é a evolução desse desvio. Os valores mudam exatamente como na figura 2, então o tremor fino está de volta (e, de novo, não é aleatoriedade).

Sua intuição de que “os dois totais sobem para a direita, com o FLOAT desviando só um pouquinho” está correta. Mas o desvio é tão pequeno em relação aos totais que, desenhadas em escala real, as duas linhas se fundiriam numa só. Por isso a figura amplia apenas a diferença. Quando a curva azul desce ou sobe, o total não está encolhendo: a direção do arredondamento mudou no caminho, empurrando o total FLOAT para baixo do DECIMAL ou puxando-o de volta para perto.

Figura 3  SUM do FLOAT − SUM do DECIMAL (evolução do desvio)

Leia as duas em par: tabela 1 = os totais em si; figura 3 = a diferença, ampliada. Declarar a coluna de dinheiro como DECIMAL é a escolha que elimina esse desvio azul antes de ele começar.

5. O famoso 0.1 + 0.2 ≠ 0.3 é a mesma história

O exemplo debatido à exaustão nos consoles das linguagens é exatamente a mesma “régua com marcações faltando”. Abrimos este artigo com totais de dinheiro que não batem; aqui, o mesmo núcleo aparece com outra roupa.

\[ \mathrm{fl}(0.1) + \mathrm{fl}(0.2) \;\neq\; \mathrm{fl}(0.3) \quad (\text{na maioria dos ambientes}) \]

No JavaScript do seu navegador, a igualdade abaixo não é estrita (o arredondamento de exibição pode escondê-la).

0.1 + 0.2 === 0.3   // false
0.1 + 0.2           // 0.30000000000000004

Repare que os números do JavaScript são de precisão dupla (FLOAT64). Suas marcações são bem mais finas que as marcações FLOAT32 da figura 1, mas o quadro é idêntico: 0.1 não cai em marcação nenhuma. Alterne a figura 1 entre 0.1, 0.2 e 0.3 e você verá cada um errando as marcações na mesma vista. O total do mês que não bate e essa curiosidade de console são a mesma marcação ausente, vista em dois lugares diferentes.

6. Na prática

  • Preços, custos unitários, alíquotas, totais de fatura — DECIMAL. Se os dados já foram gravados em FLOAT, arredondar depois nem sempre recupera dígitos que já se perderam.
  • Dados de sensores, estatísticas, features de ML — se o ruído de medição ou do modelo domina de qualquer forma, FLOAT/DOUBLE costuma ser mais que suficiente.
  • Formatação de exibição (toFixed e afins) e o tipo em que você armazena e agrega são coisas diferentes. Uma exibição caprichada não muda nada: se a coluna do banco é FLOAT, o SUM ainda pode desviar.

As tabelas de decisão e os exemplos de CREATE TABLE estão em Como escolher o tipo numérico correto em SQL; este artigo fica focado no mecanismo do desvio.

Resumo

  • O FLOAT guarda a maioria das frações decimais de forma aproximada; é o preço de lidar rápido com uma faixa enorme.
  • A causa raiz do desvio: valores decimais limpos como 0.1 não caem nas marcações binárias.
  • Um arredondamento é minúsculo; um arredondamento a cada soma fica visível no total.
  • As mesmas notas podem produzir SUMs diferentes em FLOAT e em DECIMAL (fig. 3).
  • Quando o dinheiro precisa bater exato, use DECIMAL. O erro do FLOAT é de projeto; nenhuma caçada a bugs vai eliminá-lo.

FAQ

Q1. Qual é o tamanho do erro, concretamente?

R. O FLOAT32 carrega cerca de 7 dígitos decimais significativos; o DOUBLE (FLOAT64), cerca de 15–16. “Quantos centavos vai desviar” depende das magnitudes, das quantidades e da ordem das operações, então a resposta prática é arrastar o contador da figura 2 e construir intuição.

Q2. Então DOUBLE serve para dinheiro?

R. Não. Os dígitos extras só tornam o desvio mais difícil de enxergar; não são garantia de que valores decimais sejam representados com exatidão. Para dinheiro, a resposta é DECIMAL.

Q3. É o mesmo problema do 0.1+0.2 das linguagens de programação?

R. Mesma família. O FLOAT do SQL e o tipo de ponto flutuante padrão da maioria das linguagens compartilham a aproximação binária. É uma propriedade da representação, não um “bug esquisito” do banco de dados.

Q4. Mesma tabela, mas o SUM em FLOAT sai levemente diferente entre execuções. Por quê?

R. A soma em ponto flutuante pode mudar de resultado quando a ordem das somas muda (a associatividade não vale estritamente). Se a agregação paralela ou um índice diferente muda a ordem de leitura das linhas, os últimos dígitos do SUM podem oscilar com dados idênticos. É o mesmo comportamento de “o arredondamento depende do total acumulado” da figura 2. Um SUM em DECIMAL bate seja qual for a ordem.

Q5. Já gravamos o dinheiro em FLOAT. E agora?

R. Primeiro, mude as gravações novas para DECIMAL. Corrigir o histórico exige uma decisão de negócio sobre quais dígitos ainda merecem confiança; arredondar mecanicamente nem sempre restaura a verdade. Veja a seção “dinheiro em FLOAT” de Como escolher o tipo numérico correto em SQL.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *