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.
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).
| 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 (
toFixede 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.

Leave a Reply