Revisas los tickets uno a uno y todos los importes están bien… y sin embargo, al sumar las ventas del mes, el total queda un céntimo por debajo del ingreso bancario. O guardaste precio × tipo de IVA en una columna FLOAT, y ahora el SUM se niega a cuadrar. La mayoría de estos misterios de «cada fila está bien, pero el total no» no son errores de tu código: ocurren porque los números en coma flotante (FLOAT) solo pueden almacenar la mayoría de los valores de forma aproximada.
En este artículo veremos por qué se desvían los totales, con fórmulas y gráficos interactivos que puedes manipular.
La figura 1, la figura 2 y la figura 3 son gráficos vivos que puedes tocar aquí mismo. Si lo prefieres, juega primero con las figuras y vuelve después al texto.
Si lo que buscas es orientación para elegir tipos de columna, eso está en Cómo elegir el tipo numérico correcto en SQL (el clásico fallo de «dinero en FLOAT» y una guía de decisión FLOAT vs DECIMAL).
Qué responde este artículo
| Pregunta | Dónde |
|---|---|
| ¿Por qué un decimal tan cotidiano como 0.1 da problemas? | §1 / fig. 1 |
| ¿Por qué un error invisible en una fila aparece en el total? | fig. 2 |
| ¿De verdad FLOAT y DECIMAL divergen con los mismos tickets? | fig. 3 / tabla 1 |
| ¿Qué pinta aquí el famoso 0.1 + 0.2 ≠ 0.3? | §5 |
| ¿Qué debo hacer en la práctica? | §6 / FAQ |
Para cubrir un rango enorme de números con un número fijo de bytes, FLOAT / DOUBLE almacenan la mayoría de las fracciones decimales como una aproximación redondeada al valor representable más cercano. Ese error no es un defecto: es un intercambio deliberado de exactitud por velocidad y rango.
Precisamente por eso, estos tipos encajan mal con cifras que deben cuadrar hasta el último céntimo: precios, tipos impositivos, totales de factura. Ahí la respuesta correcta es DECIMAL (NUMERIC), que garantiza los dígitos decimales. Para magnitudes que ya traen ruido de medición, como lecturas de sensores o estadísticas, el pequeño redondeo de FLOAT no suele importar en la práctica.
Una imagen mental útil: una regla a la que le faltan marcas. El decimal más corriente del mundo, 0.1, no cae en ninguna marca de la regla binaria; se ve arrastrado a la marca más próxima, y las sumas repetidas van acumulando esos arrastres. Esa es la anatomía del céntimo descuadrado en el total mensual.
1. Por qué el 0.1 decimal se le atraganta al binario
Sobre el papel, 0.1 es un decimal finito y limpio. Pero dentro de la máquina, FLOAT representa los números en binario (ceros y unos), siguiendo el estándar IEEE 754. Convierte 0.1 a binario y se repite hasta el infinito.
\[ 0.1_{10} = 0.0001100110011\ldots_{2} \]
Con un número finito de bits, hay que cortar el valor y redondearlo a la marca más cercana. Si escribimos esa operación como \(\mathrm{fl}(x)\), lo que realmente se almacena no es \(x\) sino \(\mathrm{fl}(x)\).
\[ \mathrm{almacenado}(x) = \mathrm{fl}(x) \neq x \quad (\text{para muchas fracciones decimales}) \]
Un solo redondeo suele ser diminuto. El problema aflora cuando sumas la aproximación una y otra vez.
2. Las marcas que FLOAT32 puede pisar de verdad (fig. 1)
La figura siguiente dibuja como puntos los valores que la precisión simple de IEEE 754 (FLOAT32) puede representar de verdad alrededor del objetivo que elijas. La línea discontinua es «el valor decimal que quieres»; el punto relleno fl es «la marca más cercana que FLOAT32 puede guardar». Siempre que la línea discontinua no acierte en los puntos, ese fallo es el error de redondeo.
Figura 1 Las marcas que FLOAT32 puede pisar de verdad (recta numérica y fl más cercano)
La vista se abre estrecha (8 ULP a cada lado, es decir, ocho espaciados de marca). Fíjate en la distancia entre la línea discontinua (el valor que quieres) y el punto azul fl (el valor FLOAT32 más cercano). Amplía con el deslizador y las marcas se apelotonan; estréchalo y se distingue el espaciado individual.
En un mundo ideal, un total es cada importe sumado tal cual. En FLOAT, cada suma vuelve a redondear el resultado.
\[ s_0 = 0, \qquad s_{k+1} = \mathrm{fl}(s_k + x_k) \]
Cada \(\mathrm{fl}\) individual es pequeño, pero a medida que \(k\) crece, la distancia entre \(s_n\) y el total ideal puede hacerse visible. Así es como un error que nadie nota en un ticket suelto asoma por primera vez en el SUM mensual.
# La idea (pseudocódigo) — x puede variar de un ticket a otro
s = 0
para cada importe x_k:
s = fl(s + x_k) # redondeado a la marca más cercana cada vez
# El ideal es x_1+…+x_n. s puede no coincidir.
3. El error acumulado a medida que se apilan los tickets (fig. 2)
El eje horizontal es el número de sumas \(n\); el vertical, la diferencia entre el total acumulado en FLOAT32 y el total decimal ideal. Como en una tienda de verdad, el importe \(x_k\) recorre un patrón cíclico en lugar de ser constante (el deslizador fija el importe base).
Hacia dónde cae cada redondeo depende de en qué punto entre dos marcas está el total acumulado (el espaciado de marcas, el ULP, cambia con la magnitud) y del siguiente importe \(x_k\). Por eso el error no está condenado a crecer en línea recta: puede irse a negativo, volver, o vibrar. Las ondulaciones amplias aparecen cuando el sesgo del redondeo apunta un rato en la misma dirección; el dientes-de-sierra fino viene de que la dirección del redondeo cambia al cambiar los importes.
Conviene subrayarlo: este vaivén no es azar. Suma el mismo importe al mismo total acumulado y obtendrás el mismo resultado, siempre. Los importes de la figura siguen un ciclo fijo de multiplicadores; no se lanza ningún dado en tiempo de ejecución. Parece errático, pero el mecanismo es totalmente determinista.
Figura 2 El error acumulado al seguir sumando (total FLOAT32 − ideal)
La lección: una diferencia invisible en un apunte se vuelve visible al acumular apuntes. Sube \(n_{\max}\) y compara el orden de magnitud del desvío final en el panel.
4. Sumar los mismos tickets en FLOAT y en DECIMAL (fig. 3)
La figura 2 dibujaba el tamaño del desvío. Ahora cambiamos el punto de vista y observamos cómo la misma columna de tickets se divide en dos totales, uno sumado en FLOAT y otro en DECIMAL, con la fórmula, una tabla y un gráfico avanzando al compás. Los importes recorren el mismo patrón de multiplicadores que la figura 2 (base nominal \(0.10\)).
DECIMAL suma cada \(x_k\) exactamente, en decimal. FLOAT32 intercala \(\mathrm{fl}\) en cada paso.
\[ 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 en SQL, un ejemplo mínimo queda así. El mismo SUM; solo cambian los tipos de columna.
-- Un ejemplo mínimo en SQL:
-- los importes varían por fila y, aun así, el mismo SUM puede dividirse según el tipo
SELECT SUM(amount_float) AS sum_float, -- FLOAT / REAL
SUM(amount_decimal) AS sum_decimal -- p. ej. DECIMAL(10,2)
FROM sales;
En la tabla 1, las primeras filas apenas muestran diferencia, y el desvío solo sale a la luz en la fila final, cuando los apuntes ya se han apilado (el lado DECIMAL es el total exacto, calculado a escala entera).
| Fila | Ticket (mostrado) | Acumulado FLOAT32 | Acumulado DECIMAL | Desvío (F−D) |
|---|
La figura 3 muestra el mismo proceso que la tabla 1, pero con el total DECIMAL como línea base (desvío cero). El eje vertical no es el total en sí: es SUM de FLOAT menos SUM de DECIMAL. La línea discontinua naranja marca «desvío cero (coincide con DECIMAL)»; la curva azul es la evolución de ese desvío. Los importes cambian exactamente igual que en la figura 2, así que la vibración fina reaparece (y, de nuevo, no es azar).
Tu intuición de que «ambos totales suben hacia la derecha, con FLOAT desviándose apenas un poco» es correcta. Pero el desvío es tan pequeño comparado con los totales que, dibujadas a escala completa, las dos líneas se fundirían en una. Por eso la figura amplifica solo la diferencia. Cuando la curva azul baja o sube, el total no está menguando: la dirección del redondeo cambió por el camino, empujando el total FLOAT por debajo del DECIMAL o acercándolo de nuevo.
Figura 3 SUM de FLOAT − SUM de DECIMAL (evolución del desvío)
Léelas en pareja: tabla 1 = los totales en sí; figura 3 = la diferencia, amplificada. Declarar la columna de dinero como DECIMAL es la decisión que elimina este desvío azul antes de que empiece.
5. El famoso 0.1 + 0.2 ≠ 0.3 es la misma historia
El ejemplo discutido hasta la saciedad en las consolas de los lenguajes es exactamente la misma «regla con marcas que faltan». Abrimos el artículo con totales de dinero descuadrados; aquí el mismo núcleo aparece con otro disfraz.
\[ \mathrm{fl}(0.1) + \mathrm{fl}(0.2) \;\neq\; \mathrm{fl}(0.3) \quad (\text{en la mayoría de los entornos}) \]
En el JavaScript de tu navegador, lo siguiente no es estrictamente igual (el redondeo de la presentación puede ocultarlo).
0.1 + 0.2 === 0.3 // false 0.1 + 0.2 // 0.30000000000000004
Ojo: los números de JavaScript son de doble precisión (FLOAT64). Sus marcas son mucho más finas que las marcas FLOAT32 de la figura 1, pero el dibujo es idéntico: 0.1 no cae en ninguna marca. Cambia la figura 1 entre 0.1, 0.2 y 0.3 y verás cómo cada uno falla las marcas en la misma vista. El total mensual descuadrado y esta curiosidad de consola son la misma marca ausente, vista en dos sitios distintos.
6. Cómo aplicarlo en la práctica
- Precios, costes unitarios, tipos impositivos, totales de factura — DECIMAL. Si los datos ya se guardaron en FLOAT, redondear a posteriori no siempre recupera dígitos que ya se perdieron.
- Datos de sensores, estadísticas, features de ML — si el ruido de medición o del modelo domina de todos modos, FLOAT/DOUBLE suele bastar de sobra.
- El formato de presentación (
toFixedy compañía) y el tipo en el que almacenas y agregas son cosas distintas. Una presentación pulcra no cambia nada: si la columna es FLOAT, el SUM puede seguir desviándose.
Las tablas de decisión y los ejemplos de CREATE TABLE están en Cómo elegir el tipo numérico correcto en SQL; este artículo se centra en el mecanismo del desvío.
Resumen
- FLOAT guarda la mayoría de las fracciones decimales de forma aproximada; es el precio de manejar un rango enorme a gran velocidad.
- La causa raíz del desvío: valores decimales tan limpios como 0.1 no caen en las marcas binarias.
- Un redondeo es diminuto; un redondeo en cada suma se vuelve visible en el total.
- Los mismos tickets pueden producir SUM distintos en FLOAT y en DECIMAL (fig. 3).
- Cuando el dinero debe cuadrar exacto, usa DECIMAL. El error de FLOAT es de diseño; ninguna caza de bugs lo eliminará.
FAQ
Q1. ¿Cómo de grande es el error, en concreto?
R. FLOAT32 lleva unas 7 cifras decimales significativas; DOUBLE (FLOAT64), unas 15–16. «Cuántos céntimos se desviará» depende de magnitudes, cantidades y orden de las operaciones, así que la respuesta práctica es arrastrar el contador de la figura 2 y construir intuición.
Q2. Entonces, ¿DOUBLE es aceptable para dinero?
R. No. Los dígitos extra solo hacen el desvío más difícil de ver; no garantizan que los importes decimales se representen exactos. Para dinero, la respuesta es DECIMAL.
Q3. ¿Es el mismo problema que el 0.1+0.2 de los lenguajes de programación?
R. La misma familia. El FLOAT de SQL y el tipo de coma flotante por defecto de la mayoría de lenguajes comparten la aproximación binaria. Es una propiedad de la representación, no un «bug raro» propio de la base de datos.
Q4. Misma tabla, pero el SUM de FLOAT sale ligeramente distinto entre ejecuciones. ¿Por qué?
R. La suma en coma flotante puede cambiar de resultado cuando cambia el orden de las sumas (la asociatividad no se cumple estrictamente). Si la agregación en paralelo o un índice distinto cambian el orden en que se leen las filas, los últimos dígitos del SUM pueden bailar con datos idénticos. Es el mismo comportamiento de «el redondeo depende del total acumulado» de la figura 2. Un SUM en DECIMAL coincide sea cual sea el orden.
Q5. Ya guardamos el dinero en FLOAT. ¿Y ahora qué?
R. Primero, pasa las escrituras nuevas a DECIMAL. Corregir los datos históricos exige una decisión de negocio sobre qué dígitos siguen siendo fiables; redondear mecánicamente no siempre restaura la verdad. Consulta la sección «dinero en FLOAT» de Cómo elegir el tipo numérico correcto en SQL.

Deja una respuesta