#79263·starrocks

`SUM()` over `DECIMAL` silently returns a wrapped negative number, including under `sql_mode = 'ERROR_IF_OVERFLOW'`

Author: hanke580Created Sep 17, 2026Updated Sep 17, 2026
Labelstype/bug

Steps to reproduce the behavior (Required)

sql
CREATE DATABASE IF NOT EXISTS sr_ovf_demo;

CREATE TABLE sr_ovf_demo.two (id INT, a DECIMAL(38,0))
DUPLICATE KEY(id) DISTRIBUTED BY HASH(id) BUCKETS 1
PROPERTIES('replication_num'='1');

INSERT INTO sr_ovf_demo.two VALUES
  (1, 99999999999999999999999999999999999999),
  (2, 99999999999999999999999999999999999999);
-- the exact sum is 199999999999999999999999999999999999998

-- 1. default sql_mode: binary arithmetic returns NULL, as documented
SELECT a + a AS plus, a * 2 AS times FROM sr_ovf_demo.two LIMIT 1;
-- NULL | NULL

-- 2. the same overflow through SUM: a wrapped negative number, no error
SELECT SUM(a) FROM sr_ovf_demo.two;
-- -140282366920938463463374607431768211458

-- 3. strict mode: binary arithmetic errors, as documented ...
SET sql_mode = 'ERROR_IF_OVERFLOW';
SELECT a + a FROM sr_ovf_demo.two LIMIT 1;
-- ERROR 1064 (HY000): Expr evaluate meet error: The 'add' operation involving
--                     decimal values overflows: BE:10001

-- 4. ... and SUM still returns the same wrong number
SELECT SUM(a) FROM sr_ovf_demo.two;
-- -140282366920938463463374607431768211458

-- 5. one cast, same rows, same query: the exact answer, via a 256-bit accumulator
SELECT SUM(CAST(a AS DECIMAL(50,0))) FROM sr_ovf_demo.two;
-- 199999999999999999999999999999999999998

repro.py in the linked report runs the same thing plus the row-count threshold below.

Expected behavior (Required)

The behaviour DECIMAL.md documents for arithmetic overflow, applied to the aggregate as it already is to a + a and a * 2:

  • default sql_modeNULL
  • sql_mode = 'ERROR_IF_OVERFLOW' → an error

or, since step 5 shows the engine can compute the exact value, widening the accumulator. What should not happen is a wrong number with no signal, in either mode.

docs/en/sql-reference/data-types/numeric/DECIMAL.md (on main today) says:

You can set the system variable sql_mode to ERROR_IF_OVERFLOW to allow the system to return an error instead of NULL in the case of arithmetic overflow.

It draws no distinction between aggregates and binary arithmetic.

Real behavior (Required)

SUM() accumulates into an int128 that is never checked, so it wraps. The returned value is exactly the true sum less 2^128.

The documented strict mode does not reach it. This is the sharpest part: a user who sets ERROR_IF_OVERFLOW — the documented way to say "never hand me a silently wrong number" — gets the same wrong number.

sql_mode expression result
default a + a NULL — as documented
default a * 2 NULL — as documented
default SUM(a) -140282366920938463463374607431768211458
ERROR_IF_OVERFLOW a + a error — as documented
ERROR_IF_OVERFLOW a * 2 error — as documented
ERROR_IF_OVERFLOW SUM(a) -140282366920938463463374607431768211458

It needs no extreme values — just enough rows

With an ordinary 34-digit value (10^33), the running total leaves int128 after 170 141 rows. Same column, same query, one row more:

rows exact returned
170 140 170140000000000000000000000000000000000 same ok
170 141 170141000000000000000000000000000000000 same ok
170 142 170142000000000000000000000000000000000 -170140366920938463463374607431768211456 wrong
170 143 170143000000000000000000000000000000000 -170139366920938463463374607431768211456 wrong

The crossing point is int128_max / value, so it moves with the data and is reached by row count alone. A table that has been correct for months starts returning a negative total on the row that crosses it, and every query on that column is wrong from then on.

Every accumulating path, and the storage engine

On the same two rows of 10^38 - 1:

path returned
SUM(a) -140282366920938463463374607431768211458 wrong
SUM(a) ... GROUP BY g -140282366920938463463374607431768211458 wrong
SUM(a) OVER () -140282366920938463463374607431768211458 wrong
SUM(a) + 0 -140282366920938463463374607431768211458 wrong
AVG(a) 29844659590457395982302900264509.685184 wrong
AGGREGATE KEY table, a DECIMAL(38,0) SUM -140282366920938463463374607431768211458 wrong, and persisted
SUM(DISTINCT a) 99999999999999999999999999999999999999 correct
MAX(a) 99999999999999999999999999999999999999 correct

Two of these deserve separating out.

AVG breaks an invariant that needs no arithmetic to check. The average of two identical values is that value. Both inputs are 10^38 - 1; the answer comes back as about 2.98 × 10^31, seven orders of magnitude out, because AVG divides the already-wrapped sum.

The AGGREGATE KEY case is not a query-time artifact. That table pre-aggregates on write, so the wrapped value is what the storage engine persists:

sql
CREATE TABLE sr_ovf_demo.agg (k INT, a DECIMAL(38,0) SUM)
AGGREGATE KEY(k) DISTRIBUTED BY HASH(k) BUCKETS 1 PROPERTIES('replication_num'='1');
INSERT INTO sr_ovf_demo.agg VALUES (1, 99999999999999999999999999999999999999);
INSERT INTO sr_ovf_demo.agg VALUES (1, 99999999999999999999999999999999999999);
SELECT a FROM sr_ovf_demo.agg;
-- -140282366920938463463374607431768211458

Two inserts leave the table holding a negative number. No later query can recover the truth, because the inputs are gone.

StarRocks version (Required)

4.1.4-4a9848e   (docker starrocks/allin1-ubuntu:latest)  -- reproduces
3.5.21-85fccf8                                           -- reproduces

Also present in the main branch source at 1b1505c9 (2026-09-17): the accumulator, the DECIMAL128 binding and the FE return-type rule quoted above are all from that checkout, and the documented contract is from main's DECIMAL.md.