Розуміння точності та масштабу в контексті арифметичних операцій
Давайте розбимо це і детально розглянемо деталі арифметичного оператора ділення . Ось що має сказати 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
;
І ось у вас це є. Діліться уважно, мої діти.