Почему 10 ^ 37/1 выбрасывает ошибку арифметического переполнения?

11

Продолжая свою недавнюю тенденцию играть с большими числами , я недавно выкинул ошибку, с которой столкнулся, до следующего кода:

DECLARE @big_number DECIMAL(38,0) = '1' + REPLICATE(0, 37);

PRINT @big_number + 1;
PRINT @big_number - 1;
PRINT @big_number * 1;
PRINT @big_number / 1;

Вывод, который я получаю для этого кода:

10000000000000000000000000000000000001
9999999999999999999999999999999999999
10000000000000000000000000000000000000
Msg 8115, Level 16, State 2, Line 6
Arithmetic overflow error converting expression to data type numeric.

Какие?

Почему первые 3 операции работают, а не последние? И как может быть арифметическая ошибка переполнения, если, @big_numberочевидно, может хранить выходные данные @big_number / 1?

Ник Чаммас
источник

Ответы:

18

Понимание точности и масштаба в контексте арифметических операций

Давайте разберем это и рассмотрим детали арифметического оператора деления . Вот что MSDN говорит о типах результата оператора деления :

Типы результатов

Возвращает тип данных аргумента с более высоким приоритетом. Для получения дополнительной информации см. Приоритет типа данных (Transact-SQL) .

Если целочисленный дивиденд делится на целочисленный делитель, результатом является целое число, у которого усечена любая дробная часть результата.

Мы знаем, что @big_numberэто DECIMAL. К какому типу данных относится SQL Server 1? Это бросает это к INT. Мы можем подтвердить это с помощью SQL_VARIANT_PROPERTY():

SELECT
      SQL_VARIANT_PROPERTY(1, 'BaseType')   AS [BaseType]  -- int
    , SQL_VARIANT_PROPERTY(1, 'Precision')  AS [Precision] -- 10
    , SQL_VARIANT_PROPERTY(1, 'Scale')      AS [Scale]     -- 0
;

Для ударов мы также можем заменить 1в исходном блоке кода явно введенное значение, например, DECLARE @one INT = 1;и подтвердить, что мы получаем те же результаты.

Итак, у нас есть DECIMALи INT. Так как DECIMALимеет более высокий приоритет типа данных, чем INTмы, мы знаем, что результат нашего деления будет приведен к DECIMAL.

Так в чем же проблема?

Проблема с масштабом DECIMALв выводе. Вот таблица правил о том, как SQL Server определяет точность и масштаб результатов, полученных в результате арифметических операций:

Operation                              Result precision                       Result scale *
-------------------------------------------------------------------------------------------------
e1 + e2                                max(s1, s2) + max(p1-s1, p2-s2) + 1    max(s1, s2)
e1 - e2                                max(s1, s2) + max(p1-s1, p2-s2) + 1    max(s1, s2)
e1 * e2                                p1 + p2 + 1                            s1 + s2
e1 / e2                                p1 - s1 + s2 + max(6, s1 + p2 + 1)     max(6, s1 + p2 + 1)
e1 { UNION | EXCEPT | INTERSECT } e2   max(s1, s2) + max(p1-s1, p2-s2)        max(s1, s2)
e1 % e2                                min(p1-s1, p2 -s2) + max( s1,s2 )      max(s1, s2)

* The result precision and scale have an absolute maximum of 38. When a result 
  precision is greater than 38, the corresponding scale is reduced to prevent the 
  integral part of a result from being truncated.

И вот что мы имеем для переменных в этой таблице:

e1: @big_number, a DECIMAL(38, 0)
-> p1: 38
-> s1: 0

e2: 1, an INT
-> p2: 10
-> s2: 0

e1 / e2
-> Result precision: p1 - s1 + s2 + max(6, s1 + p2 + 1) = 38 + max(6, 11) = 49
-> Result scale:                    max(6, s1 + p2 + 1) =      max(6, 11) = 11

Согласно комментарию к звездочке в таблице выше, максимальная точность DECIMALможет быть 38 . Таким образом, точность результатов уменьшается с 49 до 38, и «соответствующий масштаб уменьшается, чтобы предотвратить усечение неотъемлемой части результата». Из этого комментария не ясно, как уменьшается масштаб, но мы знаем это:

Согласно формуле в таблице, минимально возможный масштаб, который вы можете иметь после деления двух DECIMALs, равен 6.

Таким образом, мы получаем следующие результаты:

e1 / e2
-> Result precision: 49 -> reduced to 38
-> Result scale:     11 -> reduced to 6  

Note that 6 is the minimum possible scale it can be reduced to. 
It may be between 6 and 11 inclusive.

Как это объясняет арифметическое переполнение

Теперь ответ очевиден:

Выход нашей дивизии получает бросок к DECIMAL(38, 6), и DECIMAL(38, 6)не может вместить 10 37 .

При этом мы можем построить еще одно успешное деление, убедившись, что результат может соответствовать DECIMAL(38, 6):

DECLARE @big_number    DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @one_million   INT           = '1' + REPLICATE(0, 6);

PRINT @big_number / @one_million;

Результат:

10000000000000000000000000000000.000000

Обратите внимание на 6 нулей после десятичной дроби. Мы можем подтвердить тип данных DECIMAL(38, 6)результата, используя, SQL_VARIANT_PROPERTY()как указано выше:

DECLARE @big_number   DECIMAL(38,0) = '1' + REPLICATE(0, 37);
DECLARE @one_million  INT           = '1' + REPLICATE(0, 6);

SELECT
      SQL_VARIANT_PROPERTY(@big_number / @one_million, 'BaseType')  AS [BaseType]  -- decimal
    , SQL_VARIANT_PROPERTY(@big_number / @one_million, 'Precision') AS [Precision] -- 38
    , SQL_VARIANT_PROPERTY(@big_number / @one_million, 'Scale')     AS [Scale]     -- 6
;

Опасный обходной путь

Так как же нам обойти это ограничение?

Ну, это, безусловно, зависит от того, для чего вы делаете эти расчеты. Одним из решений, к которому вы можете сразу перейти, является преобразование ваших чисел FLOATв расчеты, а затем преобразование их обратно, DECIMALкогда вы закончите.

Это может работать при некоторых обстоятельствах, но вы должны быть осторожны, чтобы понять, каковы эти обстоятельства. Как мы все знаем, преобразование чисел в и из FLOATопасно и может дать вам неожиданные или неправильные результаты.

В нашем случае преобразование 10 37 в и из него FLOATдает результат, который просто неверен :

DECLARE @big_number     DECIMAL(38,0)  = '1' + REPLICATE(0, 37);
DECLARE @big_number_f   FLOAT          = CAST(@big_number AS FLOAT);

SELECT
      @big_number                           AS big_number      -- 10^37
    , @big_number_f                         AS big_number_f    -- 10^37
    , CAST(@big_number_f AS DECIMAL(38, 0)) AS big_number_f_d  -- 9999999999999999.5 * 10^21
;

И там у вас есть это. Разделите осторожно, дети мои.

Ник Чаммас
источник
2
RE: «Чистый путь». Возможно, вы захотите посмотретьSQL_VARIANT_PROPERTY
Мартин Смит,
@Martin - Не могли бы вы привести пример или краткое объяснение того, как я мог бы использовать SQL_VARIANT_PROPERTYдля выполнения делений, как то, что обсуждалось в вопросе?
Ник Чаммас
1
Здесь есть пример (в качестве альтернативы созданию новой таблицы для определения типа данных)
Мартин Смит
@ Мартин - Ах да, это намного аккуратнее!
Ник Чаммас