Понимание точности и масштаба в контексте арифметических операций
Давайте разберем это и рассмотрим детали арифметического оператора деления . Вот что 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, и «соответствующий масштаб уменьшается, чтобы предотвратить усечение неотъемлемой части результата». Из этого комментария не ясно, как уменьшается масштаб, но мы знаем это:
Согласно формуле в таблице, минимально возможный масштаб, который вы можете иметь после деления двух DECIMAL
s, равен 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
;
И там у вас есть это. Разделите осторожно, дети мои.
SQL_VARIANT_PROPERTY
SQL_VARIANT_PROPERTY
для выполнения делений, как то, что обсуждалось в вопросе?