Skip to main content

DuckDB fsum returns the wrong sum when small values are added to a large value that later cancels

· 5 min read

In DuckDB 2.0.0, fsum returns the wrong sum when small values are added to a large value that later cancels.

Reproduce it​

SELECT fsum(v) FROM (SELECT 3.6686354453820154e17::DOUBLE AS v UNION ALL SELECT 37.0::DOUBLE FROM range(14) UNION ALL SELECT -3.6686354453820154e17::DOUBLE);

DuckDB 2.0.0 returns:

512.0

The correct answer​

The correct answer is 518.0.

from fractions import Fraction as F
def fsum(values):
# the exact sum, rounded once to a double
return float(sum(F(v) for v in values))
print(fsum([3.6686354453820154e17] + [37.0] * 14 + [-3.6686354453820154e17])) # 518.0

Why DuckDB gets it wrong​

DuckDB's fsum applies its Kahan correction by folding it into the next value it adds. When that next value is large, the correction is rounded away.

Kahan Algorithm​

  1. adjusted value = value − correction
  2. new sum = old sum + adjusted value
  3. correction = (new sum − old sum) − adjusted value
  4. each part of the UNION ALL is summed in its own partial state with steps 1–3
  5. merge: each partial state, in order, adds its sum, then minus its correction, into one result state that starts empty, with steps 1–3
  6. result = sum
ValueAdjustedSumCorrectionExplanation
——00init: first partial state
3.6686354453820154e173.6686354453820154e173.6686354453820154e170First value into the sum
merge: 3.6686354453820154e173.6686354453820154e173.6686354453820154e170The partial state is merged into the empty result state: the sum
merge: −0−03.6686354453820154e170Then minus the correction
——00init: second partial state, for the 37s
3737370Whole numbers this small add exactly
3737740Whole numbers this small add exactly
37371110Whole numbers this small add exactly
37371480Whole numbers this small add exactly
37371850Whole numbers this small add exactly
37372220Whole numbers this small add exactly
37372590Whole numbers this small add exactly
37372960Whole numbers this small add exactly
37373330Whole numbers this small add exactly
37373700Whole numbers this small add exactly
37374070Whole numbers this small add exactly
37374440Whole numbers this small add exactly
37374810Whole numbers this small add exactly
37375180Whole numbers this small add exactly
merge: 5185183.6686354453820205e17−63.6686354453820154e17 + 518 rounds to 3.6686354453820154e17 + 512, and the correction catches the −6
merge: −063.6686354453820205e17−6Then minus the correction: 0 − (−6) = 6 rounds away again, so the correction stays −6
——00init: third partial state
−3.6686354453820154e17−3.6686354453820154e17−3.6686354453820154e170First value into the sum
merge: −3.6686354453820154e17−3.6686354453820154e175120−3.6686354453820154e17 − (−6) rounds to −3.6686354453820154e17, so the correction is lost
merge: −0−05120Then minus the correction

Result: fsum returns the sum, 512.0.

How to fix it​

A fixed-point sum keeps the sum exact by adding every value as a whole number of units of the smallest double.

Fixed-Point Algorithm​

  1. Start with a sum of 0
  2. Turn each value into a whole number of units of the smallest double (value × 2^1074)
  3. Add that whole number to the sum exactly

Final Step: result = sum × 2^−1074, rounded once

ValueUnitsSum (units)Explanation
——0init
3.6686354453820154e173.6686354453820154e17 × 2^10743.6686354453820154e17 × 2^1074First value into the sum, exactly
3737 × 2^1074(3.6686354453820154e17 + 37) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 74) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 111) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 148) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 185) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 222) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 259) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 296) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 333) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 370) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 407) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 444) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 481) × 2^1074Added exactly
3737 × 2^1074(3.6686354453820154e17 + 518) × 2^1074Added exactly
−3.6686354453820154e17−3.6686354453820154e17 × 2^1074518 × 2^10743.6686354453820154e17 cancels exactly

Result: 518 × 2^1074 × 2^−1074 = 518.0.

Vendor issue​

Reported to DuckDB as duckdb/duckdb#28162.

writePath​

FSUM function​

writePath's FSUM uses an exact fixed-point sum. No precision is lost because every value is added as a whole number. The sum is rounded once at the end, so the result is the most precise double. The result does not depend on row order.

About us​

writePath is an AWS Marketplace SaaS for sub-millisecond aggregations over the first or last N rows of your data with no pre-computation. It offers 65 high-precision aggregation functions tested against reference implementations.

www.thewritepath.net